mirror of
https://github.com/bitwarden/server.git
synced 2025-05-22 20:11:04 -05:00
Merge branch 'main' into test-container-workflow-changes
This commit is contained in:
commit
88159d16bb
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@ -90,6 +90,9 @@ src/Admin/Views/Tools @bitwarden/team-billing-dev
|
|||||||
.github/workflows/test-database.yml @bitwarden/team-platform-dev
|
.github/workflows/test-database.yml @bitwarden/team-platform-dev
|
||||||
.github/workflows/test.yml @bitwarden/team-platform-dev
|
.github/workflows/test.yml @bitwarden/team-platform-dev
|
||||||
**/*Platform* @bitwarden/team-platform-dev
|
**/*Platform* @bitwarden/team-platform-dev
|
||||||
|
**/.dockerignore @bitwarden/team-platform-dev
|
||||||
|
**/Dockerfile @bitwarden/team-platform-dev
|
||||||
|
**/entrypoint.sh @bitwarden/team-platform-dev
|
||||||
|
|
||||||
# Multiple owners - DO NOT REMOVE (BRE)
|
# Multiple owners - DO NOT REMOVE (BRE)
|
||||||
**/packages.lock.json
|
**/packages.lock.json
|
||||||
|
66
.github/renovate.json5
vendored
66
.github/renovate.json5
vendored
@ -20,7 +20,7 @@
|
|||||||
],
|
],
|
||||||
commitMessagePrefix: "[deps] BRE:",
|
commitMessagePrefix: "[deps] BRE:",
|
||||||
reviewers: ["team:dept-bre"],
|
reviewers: ["team:dept-bre"],
|
||||||
addLabels: ["hold"]
|
addLabels: ["hold"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
groupName: "dockerfile minor",
|
groupName: "dockerfile minor",
|
||||||
@ -37,6 +37,16 @@
|
|||||||
matchManagers: ["github-actions"],
|
matchManagers: ["github-actions"],
|
||||||
matchUpdateTypes: ["minor"],
|
matchUpdateTypes: ["minor"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// For any Microsoft.Extensions.* and Microsoft.AspNetCore.* packages, we want to create PRs for patch updates.
|
||||||
|
// This overrides the default that ignores patch updates for nuget dependencies.
|
||||||
|
matchPackageNames: [
|
||||||
|
"/^Microsoft\\.Extensions\\./",
|
||||||
|
"/^Microsoft\\.AspNetCore\\./",
|
||||||
|
],
|
||||||
|
matchUpdateTypes: ["patch"],
|
||||||
|
dependencyDashboardApproval: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
matchManagers: ["dockerfile", "docker-compose"],
|
matchManagers: ["dockerfile", "docker-compose"],
|
||||||
commitMessagePrefix: "[deps] BRE:",
|
commitMessagePrefix: "[deps] BRE:",
|
||||||
@ -59,6 +69,7 @@
|
|||||||
"DuoUniversal",
|
"DuoUniversal",
|
||||||
"Fido2.AspNet",
|
"Fido2.AspNet",
|
||||||
"Duende.IdentityServer",
|
"Duende.IdentityServer",
|
||||||
|
"Microsoft.AspNetCore.Authentication.JwtBearer",
|
||||||
"Microsoft.Extensions.Identity.Stores",
|
"Microsoft.Extensions.Identity.Stores",
|
||||||
"Otp.NET",
|
"Otp.NET",
|
||||||
"Sustainsys.Saml2.AspNetCore2",
|
"Sustainsys.Saml2.AspNetCore2",
|
||||||
@ -79,8 +90,6 @@
|
|||||||
"CsvHelper",
|
"CsvHelper",
|
||||||
"Kralizek.AutoFixture.Extensions.MockHttp",
|
"Kralizek.AutoFixture.Extensions.MockHttp",
|
||||||
"Microsoft.AspNetCore.Mvc.Testing",
|
"Microsoft.AspNetCore.Mvc.Testing",
|
||||||
"Microsoft.Extensions.Logging",
|
|
||||||
"Microsoft.Extensions.Logging.Console",
|
|
||||||
"Newtonsoft.Json",
|
"Newtonsoft.Json",
|
||||||
"NSubstitute",
|
"NSubstitute",
|
||||||
"Sentry.Serilog",
|
"Sentry.Serilog",
|
||||||
@ -100,9 +109,9 @@
|
|||||||
reviewers: ["team:team-billing-dev"],
|
reviewers: ["team:team-billing-dev"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
matchPackagePatterns: ["^Microsoft.Extensions.Logging"],
|
matchPackageNames: ["/^Microsoft\\.EntityFrameworkCore\\./", "/^dotnet-ef/"],
|
||||||
groupName: "Microsoft.Extensions.Logging",
|
groupName: "EntityFrameworkCore",
|
||||||
description: "Group Microsoft.Extensions.Logging to exclude them from the dotnet monorepo preset",
|
description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
matchPackageNames: [
|
matchPackageNames: [
|
||||||
@ -117,9 +126,6 @@
|
|||||||
"Microsoft.EntityFrameworkCore.Relational",
|
"Microsoft.EntityFrameworkCore.Relational",
|
||||||
"Microsoft.EntityFrameworkCore.Sqlite",
|
"Microsoft.EntityFrameworkCore.Sqlite",
|
||||||
"Microsoft.EntityFrameworkCore.SqlServer",
|
"Microsoft.EntityFrameworkCore.SqlServer",
|
||||||
"Microsoft.Extensions.Caching.Cosmos",
|
|
||||||
"Microsoft.Extensions.Caching.SqlServer",
|
|
||||||
"Microsoft.Extensions.Caching.StackExchangeRedis",
|
|
||||||
"Npgsql.EntityFrameworkCore.PostgreSQL",
|
"Npgsql.EntityFrameworkCore.PostgreSQL",
|
||||||
"Pomelo.EntityFrameworkCore.MySql",
|
"Pomelo.EntityFrameworkCore.MySql",
|
||||||
],
|
],
|
||||||
@ -142,56 +148,40 @@
|
|||||||
"Azure.Messaging.ServiceBus",
|
"Azure.Messaging.ServiceBus",
|
||||||
"Azure.Storage.Blobs",
|
"Azure.Storage.Blobs",
|
||||||
"Azure.Storage.Queues",
|
"Azure.Storage.Queues",
|
||||||
"Microsoft.AspNetCore.Authentication.JwtBearer",
|
"LaunchDarkly.ServerSdk",
|
||||||
"Microsoft.AspNetCore.Http",
|
"Microsoft.AspNetCore.Http",
|
||||||
|
"Microsoft.AspNetCore.SignalR.Protocols.MessagePack",
|
||||||
|
"Microsoft.AspNetCore.SignalR.StackExchangeRedis",
|
||||||
|
"Microsoft.Extensions.Configuration.EnvironmentVariables",
|
||||||
|
"Microsoft.Extensions.Configuration.UserSecrets",
|
||||||
|
"Microsoft.Extensions.Configuration",
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions",
|
||||||
|
"Microsoft.Extensions.DependencyInjection",
|
||||||
|
"Microsoft.Extensions.Logging",
|
||||||
|
"Microsoft.Extensions.Logging.Console",
|
||||||
|
"Microsoft.Extensions.Caching.Cosmos",
|
||||||
|
"Microsoft.Extensions.Caching.SqlServer",
|
||||||
|
"Microsoft.Extensions.Caching.StackExchangeRedis",
|
||||||
"Quartz",
|
"Quartz",
|
||||||
],
|
],
|
||||||
description: "Platform owned dependencies",
|
description: "Platform owned dependencies",
|
||||||
commitMessagePrefix: "[deps] Platform:",
|
commitMessagePrefix: "[deps] Platform:",
|
||||||
reviewers: ["team:team-platform-dev"],
|
reviewers: ["team:team-platform-dev"],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
matchPackagePatterns: ["EntityFrameworkCore", "^dotnet-ef"],
|
|
||||||
groupName: "EntityFrameworkCore",
|
|
||||||
description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
matchPackageNames: [
|
matchPackageNames: [
|
||||||
"AutoMapper.Extensions.Microsoft.DependencyInjection",
|
"AutoMapper.Extensions.Microsoft.DependencyInjection",
|
||||||
"AWSSDK.SimpleEmail",
|
"AWSSDK.SimpleEmail",
|
||||||
"AWSSDK.SQS",
|
"AWSSDK.SQS",
|
||||||
"Handlebars.Net",
|
"Handlebars.Net",
|
||||||
"LaunchDarkly.ServerSdk",
|
|
||||||
"MailKit",
|
"MailKit",
|
||||||
"Microsoft.AspNetCore.SignalR.Protocols.MessagePack",
|
|
||||||
"Microsoft.AspNetCore.SignalR.StackExchangeRedis",
|
|
||||||
"Microsoft.Azure.NotificationHubs",
|
"Microsoft.Azure.NotificationHubs",
|
||||||
"Microsoft.Extensions.Configuration.EnvironmentVariables",
|
|
||||||
"Microsoft.Extensions.Configuration.UserSecrets",
|
|
||||||
"Microsoft.Extensions.Configuration",
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions",
|
|
||||||
"Microsoft.Extensions.DependencyInjection",
|
|
||||||
"SendGrid",
|
"SendGrid",
|
||||||
],
|
],
|
||||||
description: "Tools owned dependencies",
|
description: "Tools owned dependencies",
|
||||||
commitMessagePrefix: "[deps] Tools:",
|
commitMessagePrefix: "[deps] Tools:",
|
||||||
reviewers: ["team:team-tools-dev"],
|
reviewers: ["team:team-tools-dev"],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
matchPackagePatterns: ["^Microsoft.AspNetCore.SignalR"],
|
|
||||||
groupName: "SignalR",
|
|
||||||
description: "Group SignalR to exclude them from the dotnet monorepo preset",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
matchPackagePatterns: ["^Microsoft.Extensions.Configuration"],
|
|
||||||
groupName: "Microsoft.Extensions.Configuration",
|
|
||||||
description: "Group Microsoft.Extensions.Configuration to exclude them from the dotnet monorepo preset",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
matchPackagePatterns: ["^Microsoft.Extensions.DependencyInjection"],
|
|
||||||
groupName: "Microsoft.Extensions.DependencyInjection",
|
|
||||||
description: "Group Microsoft.Extensions.DependencyInjection to exclude them from the dotnet monorepo preset",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
matchPackageNames: [
|
matchPackageNames: [
|
||||||
"AngleSharp",
|
"AngleSharp",
|
||||||
|
302
.github/workflows/build.yml
vendored
302
.github/workflows/build.yml
vendored
@ -14,11 +14,12 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
||||||
|
_GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: Lint
|
name: Lint
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
@ -31,12 +32,11 @@ jobs:
|
|||||||
- name: Verify format
|
- name: Verify format
|
||||||
run: dotnet format --verify-no-changes
|
run: dotnet format --verify-no-changes
|
||||||
|
|
||||||
build-container:
|
build-artifacts:
|
||||||
name: Build container images
|
name: Build artifacts
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-22.04
|
||||||
permissions:
|
needs:
|
||||||
id-token: write
|
- lint
|
||||||
security-events: write
|
|
||||||
outputs:
|
outputs:
|
||||||
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
|
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
|
||||||
strategy:
|
strategy:
|
||||||
@ -45,10 +45,9 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- project_name: Admin
|
- project_name: Admin
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
|
node: true
|
||||||
- project_name: Api
|
- project_name: Api
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
- project_name: Attachments
|
|
||||||
base_path: ./util
|
|
||||||
- project_name: Billing
|
- project_name: Billing
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
- project_name: Events
|
- project_name: Events
|
||||||
@ -59,20 +58,21 @@ jobs:
|
|||||||
base_path: ./src
|
base_path: ./src
|
||||||
- project_name: Identity
|
- project_name: Identity
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
- project_name: MsSql
|
|
||||||
base_path: ./util
|
|
||||||
- project_name: MsSqlMigratorUtility
|
- project_name: MsSqlMigratorUtility
|
||||||
base_path: ./util
|
base_path: ./util
|
||||||
- project_name: Nginx
|
dotnet: true
|
||||||
base_path: ./util
|
|
||||||
- project_name: Notifications
|
- project_name: Notifications
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
- project_name: Scim
|
- project_name: Scim
|
||||||
base_path: ./bitwarden_license/src
|
base_path: ./bitwarden_license/src
|
||||||
|
dotnet: true
|
||||||
|
- project_name: Server
|
||||||
|
base_path: ./util
|
||||||
- project_name: Setup
|
- project_name: Setup
|
||||||
base_path: ./util
|
base_path: ./util
|
||||||
- project_name: Sso
|
- project_name: Sso
|
||||||
base_path: ./bitwarden_license/src
|
base_path: ./bitwarden_license/src
|
||||||
|
node: true
|
||||||
steps:
|
steps:
|
||||||
- name: Check secrets
|
- name: Check secrets
|
||||||
id: check-secrets
|
id: check-secrets
|
||||||
@ -87,6 +87,116 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
|
- name: Set up .NET
|
||||||
|
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||||
|
with:
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: "**/package-lock.json"
|
||||||
|
node-version: "16"
|
||||||
|
|
||||||
|
- name: Print environment
|
||||||
|
run: |
|
||||||
|
whoami
|
||||||
|
dotnet --info
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
|
- name: Build node
|
||||||
|
if: ${{ matrix.node }}
|
||||||
|
working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }}
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Publish project
|
||||||
|
working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }}
|
||||||
|
run: |
|
||||||
|
echo "Publish"
|
||||||
|
dotnet publish -c "Release" -o obj/build-output/publish
|
||||||
|
|
||||||
|
cd obj/build-output/publish
|
||||||
|
zip -r ${{ matrix.project_name }}.zip .
|
||||||
|
mv ${{ matrix.project_name }}.zip ../../../
|
||||||
|
|
||||||
|
pwd
|
||||||
|
ls -atlh ../../../
|
||||||
|
|
||||||
|
- name: Upload project artifact
|
||||||
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.project_name }}.zip
|
||||||
|
path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build-docker:
|
||||||
|
name: Build Docker images
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
id-token: write
|
||||||
|
needs:
|
||||||
|
- build-artifacts
|
||||||
|
if: ${{ needs.build-artifacts.outputs.has_secrets == 'true' }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- project_name: Admin
|
||||||
|
base_path: ./src
|
||||||
|
dotnet: true
|
||||||
|
- project_name: Api
|
||||||
|
base_path: ./src
|
||||||
|
dotnet: true
|
||||||
|
- project_name: Attachments
|
||||||
|
base_path: ./util
|
||||||
|
- project_name: Billing
|
||||||
|
base_path: ./src
|
||||||
|
dotnet: true
|
||||||
|
- project_name: Events
|
||||||
|
base_path: ./src
|
||||||
|
dotnet: true
|
||||||
|
- project_name: EventsProcessor
|
||||||
|
base_path: ./src
|
||||||
|
dotnet: true
|
||||||
|
- project_name: Icons
|
||||||
|
base_path: ./src
|
||||||
|
dotnet: true
|
||||||
|
- project_name: Identity
|
||||||
|
base_path: ./src
|
||||||
|
dotnet: true
|
||||||
|
- project_name: MsSql
|
||||||
|
base_path: ./util
|
||||||
|
- project_name: MsSqlMigratorUtility
|
||||||
|
base_path: ./util
|
||||||
|
dotnet: true
|
||||||
|
- project_name: Nginx
|
||||||
|
base_path: ./util
|
||||||
|
- project_name: Notifications
|
||||||
|
base_path: ./src
|
||||||
|
dotnet: true
|
||||||
|
- project_name: Scim
|
||||||
|
base_path: ./bitwarden_license/src
|
||||||
|
dotnet: true
|
||||||
|
- project_name: Server
|
||||||
|
base_path: ./util
|
||||||
|
dotnet: true
|
||||||
|
- project_name: Setup
|
||||||
|
base_path: ./util
|
||||||
|
dotnet: true
|
||||||
|
- project_name: Sso
|
||||||
|
base_path: ./bitwarden_license/src
|
||||||
|
dotnet: true
|
||||||
|
steps:
|
||||||
|
- name: Check out repo
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
- name: Check branch to publish
|
- name: Check branch to publish
|
||||||
env:
|
env:
|
||||||
PUBLISH_BRANCHES: "main,rc,hotfix-rc"
|
PUBLISH_BRANCHES: "main,rc,hotfix-rc"
|
||||||
@ -100,13 +210,6 @@ jobs:
|
|||||||
echo "is_publish_branch=false" >> $GITHUB_ENV
|
echo "is_publish_branch=false" >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
########## Set up Docker ##########
|
|
||||||
- name: Set up QEMU emulators
|
|
||||||
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
|
|
||||||
|
|
||||||
########## ACRs ##########
|
########## ACRs ##########
|
||||||
- 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
|
||||||
@ -116,22 +219,40 @@ jobs:
|
|||||||
- name: Log in to ACR - production subscription
|
- name: Log in to ACR - production subscription
|
||||||
run: az acr login -n bitwardenprod
|
run: az acr login -n bitwardenprod
|
||||||
|
|
||||||
########## Generate image tag and build container image ##########
|
- name: Log in to Azure - CI subscription
|
||||||
- name: Generate container image tag
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
|
||||||
|
- name: Retrieve GitHub PAT secrets
|
||||||
|
id: retrieve-secret-pat
|
||||||
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
|
with:
|
||||||
|
keyvault: "bitwarden-ci"
|
||||||
|
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
|
########## Generate image tag and build Docker image ##########
|
||||||
|
- name: Generate Docker image tag
|
||||||
id: tag
|
id: tag
|
||||||
run: |
|
run: |
|
||||||
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
|
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" || "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then
|
||||||
IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s#/#-#g")
|
IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize branch name to alphanumeric only
|
||||||
else
|
else
|
||||||
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||||
|
SANITIZED_REPO_NAME=$(echo "$_GITHUB_PR_REPO_NAME" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize repo name to alphanumeric only
|
||||||
|
IMAGE_TAG=$SANITIZED_REPO_NAME-$IMAGE_TAG # Add repo name to the tag
|
||||||
|
IMAGE_TAG=${IMAGE_TAG:0:128} # Limit to 128 characters, as that's the max length for Docker image tags
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$IMAGE_TAG" == "main" ]]; then
|
if [[ "$IMAGE_TAG" == "main" ]]; then
|
||||||
IMAGE_TAG=dev
|
IMAGE_TAG=dev
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||||||
echo "### :mega: Container Image Tag: $IMAGE_TAG" >> $GITHUB_STEP_SUMMARY
|
echo "### :mega: Docker Image Tag: $IMAGE_TAG" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
- name: Set up project name
|
- name: Set up project name
|
||||||
id: setup
|
id: setup
|
||||||
@ -156,26 +277,30 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Generate image full name
|
- name: Get build artifact
|
||||||
id: cache-name
|
if: ${{ matrix.dotnet }}
|
||||||
env:
|
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||||
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
with:
|
||||||
run: echo "name=${_AZ_REGISTRY}/${PROJECT_NAME}:buildcache" >> $GITHUB_OUTPUT
|
name: ${{ matrix.project_name }}.zip
|
||||||
|
|
||||||
- name: Build Container image
|
- name: Set up build artifact
|
||||||
id: build-container
|
if: ${{ matrix.dotnet }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ${{ matrix.base_path}}/${{ matrix.project_name }}/obj/build-output/publish
|
||||||
|
unzip ${{ matrix.project_name }}.zip \
|
||||||
|
-d ${{ matrix.base_path }}/${{ matrix.project_name }}/obj/build-output/publish
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
id: build-docker
|
||||||
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0
|
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0
|
||||||
with:
|
with:
|
||||||
cache-from: type=registry,ref=${{ steps.cache-name.outputs.name }}
|
context: ${{ matrix.base_path }}/${{ matrix.project_name }}
|
||||||
cache-to: type=registry,ref=${{ steps.cache-name.outputs.name}},mode=max
|
|
||||||
context: .
|
|
||||||
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
|
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
|
||||||
platforms: |
|
platforms: linux/amd64
|
||||||
linux/amd64,
|
|
||||||
linux/arm/v7,
|
|
||||||
linux/arm64
|
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.image-tags.outputs.tags }}
|
tags: ${{ steps.image-tags.outputs.tags }}
|
||||||
|
secrets: |
|
||||||
|
"GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}"
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
@ -184,7 +309,7 @@ jobs:
|
|||||||
- name: Sign image with Cosign
|
- name: Sign image with Cosign
|
||||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
env:
|
env:
|
||||||
DIGEST: ${{ steps.build-container.outputs.digest }}
|
DIGEST: ${{ steps.build-docker.outputs.digest }}
|
||||||
TAGS: ${{ steps.image-tags.outputs.tags }}
|
TAGS: ${{ steps.image-tags.outputs.tags }}
|
||||||
run: |
|
run: |
|
||||||
IFS="," read -a tags <<< "${TAGS}"
|
IFS="," read -a tags <<< "${TAGS}"
|
||||||
@ -194,7 +319,7 @@ jobs:
|
|||||||
done
|
done
|
||||||
cosign sign --yes ${images}
|
cosign sign --yes ${images}
|
||||||
|
|
||||||
- name: Scan container image
|
- name: Scan Docker image
|
||||||
id: container-scan
|
id: container-scan
|
||||||
uses: anchore/scan-action@abae793926ec39a78ab18002bc7fc45bbbd94342 # v6.0.0
|
uses: anchore/scan-action@abae793926ec39a78ab18002bc7fc45bbbd94342 # v6.0.0
|
||||||
with:
|
with:
|
||||||
@ -209,10 +334,10 @@ jobs:
|
|||||||
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
|
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
|
||||||
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
||||||
|
|
||||||
build-stub-swagger:
|
upload:
|
||||||
name: Build Docker-Stub/Swagger
|
name: Upload
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-22.04
|
||||||
needs: build-container
|
needs: build-docker
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
@ -227,11 +352,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
|
|
||||||
- name: Login to PROD ACR
|
- name: Log in to ACR - production subscription
|
||||||
run: az acr login -n ${_AZ_REGISTRY%.azurecr.io}
|
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
||||||
|
|
||||||
- name: Restore
|
|
||||||
run: dotnet tool restore
|
|
||||||
|
|
||||||
- name: Make Docker stubs
|
- name: Make Docker stubs
|
||||||
if: |
|
if: |
|
||||||
@ -326,10 +448,8 @@ jobs:
|
|||||||
- name: Build Public API Swagger
|
- name: Build Public API Swagger
|
||||||
run: |
|
run: |
|
||||||
cd ./src/Api
|
cd ./src/Api
|
||||||
echo "Restore"
|
echo "Restore tools"
|
||||||
dotnet restore --locked-mode
|
dotnet tool restore
|
||||||
echo "Clean"
|
|
||||||
dotnet clean -c "Release" -o obj/build-output/publish
|
|
||||||
echo "Publish"
|
echo "Publish"
|
||||||
dotnet publish -c "Release" -o obj/build-output/publish
|
dotnet publish -c "Release" -o obj/build-output/publish
|
||||||
|
|
||||||
@ -392,7 +512,9 @@ jobs:
|
|||||||
|
|
||||||
build-mssqlmigratorutility:
|
build-mssqlmigratorutility:
|
||||||
name: Build MSSQL migrator utility
|
name: Build MSSQL migrator utility
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-22.04
|
||||||
|
needs:
|
||||||
|
- lint
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
@ -420,11 +542,6 @@ jobs:
|
|||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Restore project
|
|
||||||
run: |
|
|
||||||
echo "Restore"
|
|
||||||
dotnet restore --locked-mode
|
|
||||||
|
|
||||||
- name: Publish project
|
- name: Publish project
|
||||||
run: |
|
run: |
|
||||||
dotnet publish -c "Release" -o obj/build-output/publish -r ${{ matrix.target }} -p:PublishSingleFile=true \
|
dotnet publish -c "Release" -o obj/build-output/publish -r ${{ matrix.target }} -p:PublishSingleFile=true \
|
||||||
@ -451,8 +568,9 @@ jobs:
|
|||||||
if: |
|
if: |
|
||||||
github.event_name != 'pull_request'
|
github.event_name != 'pull_request'
|
||||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-22.04
|
||||||
needs: build-container
|
needs:
|
||||||
|
- build-docker
|
||||||
steps:
|
steps:
|
||||||
- name: Log in to Azure - CI subscription
|
- name: Log in to Azure - CI subscription
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
@ -484,8 +602,9 @@ jobs:
|
|||||||
trigger-k8s-deploy:
|
trigger-k8s-deploy:
|
||||||
name: Trigger k8s deploy
|
name: Trigger k8s deploy
|
||||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-22.04
|
||||||
needs: build-container
|
needs:
|
||||||
|
- build-docker
|
||||||
steps:
|
steps:
|
||||||
- name: Log in to Azure - CI subscription
|
- name: Log in to Azure - CI subscription
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
@ -515,49 +634,13 @@ jobs:
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
trigger-ee-updates:
|
setup-ephemeral-environment:
|
||||||
name: Trigger Ephemeral Environment updates
|
name: Setup Ephemeral Environment
|
||||||
if: |
|
|
||||||
needs.build-container.outputs.has_secrets == 'true'
|
|
||||||
&& github.event_name == 'pull_request'
|
|
||||||
&& contains(github.event.pull_request.labels.*.name, 'ephemeral-environment')
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
needs: build-container
|
|
||||||
steps:
|
|
||||||
- name: Log in to Azure - CI subscription
|
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
|
||||||
with:
|
|
||||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
|
||||||
|
|
||||||
- name: Retrieve GitHub PAT secrets
|
|
||||||
id: retrieve-secret-pat
|
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
|
||||||
with:
|
|
||||||
keyvault: "bitwarden-ci"
|
|
||||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
|
||||||
|
|
||||||
- name: Trigger Ephemeral Environment update
|
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
|
||||||
with:
|
|
||||||
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
|
||||||
script: |
|
|
||||||
await github.rest.actions.createWorkflowDispatch({
|
|
||||||
owner: 'bitwarden',
|
|
||||||
repo: 'devops',
|
|
||||||
workflow_id: '_update_ephemeral_tags.yml',
|
|
||||||
ref: 'main',
|
|
||||||
inputs: {
|
|
||||||
ephemeral_env_branch: process.env.GITHUB_HEAD_REF
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
trigger-ephemeral-environment-sync:
|
|
||||||
name: Trigger Ephemeral Environment Sync
|
|
||||||
needs:
|
needs:
|
||||||
- build-container
|
- build-artifacts
|
||||||
- trigger-ee-updates
|
- build-docker
|
||||||
if: |
|
if: |
|
||||||
needs.build-container.outputs.has_secrets == 'true'
|
needs.build-artifacts.outputs.has_secrets == 'true'
|
||||||
&& github.event_name == 'pull_request'
|
&& github.event_name == 'pull_request'
|
||||||
&& contains(github.event.pull_request.labels.*.name, 'ephemeral-environment')
|
&& contains(github.event.pull_request.labels.*.name, 'ephemeral-environment')
|
||||||
uses: bitwarden/gh-actions/.github/workflows/_ephemeral_environment_manager.yml@main
|
uses: bitwarden/gh-actions/.github/workflows/_ephemeral_environment_manager.yml@main
|
||||||
@ -569,16 +652,15 @@ jobs:
|
|||||||
check-failures:
|
check-failures:
|
||||||
name: Check for failures
|
name: Check for failures
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- lint
|
- lint
|
||||||
- build-container
|
- build-artifacts
|
||||||
- build-stub-swagger
|
- build-docker
|
||||||
|
- upload
|
||||||
- build-mssqlmigratorutility
|
- build-mssqlmigratorutility
|
||||||
- self-host-build
|
- self-host-build
|
||||||
- trigger-k8s-deploy
|
- trigger-k8s-deploy
|
||||||
- trigger-ee-updates
|
|
||||||
- trigger-ephemeral-environment-sync
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check if any job failed
|
- name: Check if any job failed
|
||||||
if: |
|
if: |
|
||||||
|
4
.github/workflows/build_target.yml
vendored
4
.github/workflows/build_target.yml
vendored
@ -2,7 +2,9 @@ name: Build on PR Target
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize, reopened]
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
|
8
.github/workflows/scan.yml
vendored
8
.github/workflows/scan.yml
vendored
@ -7,8 +7,14 @@ on:
|
|||||||
- "main"
|
- "main"
|
||||||
- "rc"
|
- "rc"
|
||||||
- "hotfix-rc"
|
- "hotfix-rc"
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
branches-ignore:
|
||||||
|
- main
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize, reopened]
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-run:
|
check-run:
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
<Version>2025.5.0</Version>
|
<Version>2025.5.1</Version>
|
||||||
|
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
@ -129,6 +129,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "t
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.IntegrationTest", "test\Core.IntegrationTest\Core.IntegrationTest.csproj", "{3631BA42-6731-4118-A917-DAA43C5032B9}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.IntegrationTest", "test\Core.IntegrationTest\Core.IntegrationTest.csproj", "{3631BA42-6731-4118-A917-DAA43C5032B9}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seeder", "util\Seeder\Seeder.csproj", "{9A612EBA-1C0E-42B8-982B-62F0EE81000A}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -325,6 +329,14 @@ Global
|
|||||||
{3631BA42-6731-4118-A917-DAA43C5032B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{3631BA42-6731-4118-A917-DAA43C5032B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.Build.0 = Release|Any CPU
|
{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -377,6 +389,8 @@ Global
|
|||||||
{4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
{3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
|
{9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||||
|
{17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||||
|
@ -3,9 +3,9 @@ using Bit.Core.AdminConsole.Enums.Provider;
|
|||||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
@ -7,13 +7,12 @@ using Bit.Core.AdminConsole.Repositories;
|
|||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.AdminConsole.Providers;
|
namespace Bit.Commercial.Core.AdminConsole.Providers;
|
||||||
@ -23,7 +22,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IOrganizationService _organizationService;
|
|
||||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||||
private readonly IStripeAdapter _stripeAdapter;
|
private readonly IStripeAdapter _stripeAdapter;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
@ -31,26 +29,22 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
private readonly ISubscriberService _subscriberService;
|
private readonly ISubscriberService _subscriberService;
|
||||||
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
private readonly IAutomaticTaxStrategy _automaticTaxStrategy;
|
|
||||||
|
|
||||||
public RemoveOrganizationFromProviderCommand(
|
public RemoveOrganizationFromProviderCommand(
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationService organizationService,
|
|
||||||
IProviderOrganizationRepository providerOrganizationRepository,
|
IProviderOrganizationRepository providerOrganizationRepository,
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IProviderBillingService providerBillingService,
|
IProviderBillingService providerBillingService,
|
||||||
ISubscriberService subscriberService,
|
ISubscriberService subscriberService,
|
||||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
||||||
IPricingClient pricingClient,
|
IPricingClient pricingClient)
|
||||||
[FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy)
|
|
||||||
{
|
{
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationService = organizationService;
|
|
||||||
_providerOrganizationRepository = providerOrganizationRepository;
|
_providerOrganizationRepository = providerOrganizationRepository;
|
||||||
_stripeAdapter = stripeAdapter;
|
_stripeAdapter = stripeAdapter;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
@ -58,7 +52,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
_subscriberService = subscriberService;
|
_subscriberService = subscriberService;
|
||||||
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
_automaticTaxStrategy = automaticTaxStrategy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RemoveOrganizationFromProvider(
|
public async Task RemoveOrganizationFromProvider(
|
||||||
@ -76,7 +69,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
|
|
||||||
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(
|
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(
|
||||||
providerOrganization.OrganizationId,
|
providerOrganization.OrganizationId,
|
||||||
Array.Empty<Guid>(),
|
[],
|
||||||
includeProvider: false))
|
includeProvider: false))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Organization must have at least one confirmed owner.");
|
throw new BadRequestException("Organization must have at least one confirmed owner.");
|
||||||
@ -101,7 +94,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// When a client organization is unlinked from a provider, we have to check if they're Stripe-enabled
|
/// When a client organization is unlinked from a provider, we have to check if they're Stripe-enabled
|
||||||
/// and, if they are, we remove their MSP discount and set their Subscription to `send_invoice`. This is because
|
/// and, if they are, we remove their MSP discount and set their Subscription to `send_invoice`. This is because
|
||||||
/// the provider's payment method will be removed from their Stripe customer causing ensuing charges to fail. Lastly,
|
/// the provider's payment method will be removed from their Stripe customer, causing ensuing charges to fail. Lastly,
|
||||||
/// we email the organization owners letting them know they need to add a new payment method.
|
/// we email the organization owners letting them know they need to add a new payment method.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task ResetOrganizationBillingAsync(
|
private async Task ResetOrganizationBillingAsync(
|
||||||
@ -141,15 +134,18 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }]
|
Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }]
|
||||||
};
|
};
|
||||||
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
|
var setNonUSBusinessUseToReverseCharge = _featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||||
|
|
||||||
|
if (setNonUSBusinessUseToReverseCharge)
|
||||||
{
|
{
|
||||||
_automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer);
|
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||||
}
|
}
|
||||||
else
|
else if (customer.HasRecognizedTaxLocation())
|
||||||
{
|
{
|
||||||
subscriptionCreateOptions.AutomaticTax ??= new SubscriptionAutomaticTaxOptions
|
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||||
{
|
{
|
||||||
Enabled = true
|
Enabled = customer.Address.Country == "US" ||
|
||||||
|
customer.TaxIds.Any()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,7 +182,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
await _mailService.SendProviderUpdatePaymentMethod(
|
await _mailService.SendProviderUpdatePaymentMethod(
|
||||||
organization.Id,
|
organization.Id,
|
||||||
organization.Name,
|
organization.Name,
|
||||||
provider.Name,
|
provider.Name!,
|
||||||
organizationOwnerEmails);
|
organizationOwnerEmails);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,13 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Business.Provider;
|
using Bit.Core.AdminConsole.Models.Business.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -53,6 +54,7 @@ public class ProviderService : IProviderService
|
|||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
private readonly IProviderBillingService _providerBillingService;
|
private readonly IProviderBillingService _providerBillingService;
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
|
private readonly IProviderClientOrganizationSignUpCommand _providerClientOrganizationSignUpCommand;
|
||||||
|
|
||||||
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
||||||
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
|
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
|
||||||
@ -61,7 +63,8 @@ public class ProviderService : IProviderService
|
|||||||
IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
|
IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
|
||||||
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,
|
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,
|
||||||
IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,
|
IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,
|
||||||
IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient)
|
IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient,
|
||||||
|
IProviderClientOrganizationSignUpCommand providerClientOrganizationSignUpCommand)
|
||||||
{
|
{
|
||||||
_providerRepository = providerRepository;
|
_providerRepository = providerRepository;
|
||||||
_providerUserRepository = providerUserRepository;
|
_providerUserRepository = providerUserRepository;
|
||||||
@ -81,6 +84,7 @@ public class ProviderService : IProviderService
|
|||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_providerBillingService = providerBillingService;
|
_providerBillingService = providerBillingService;
|
||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
|
_providerClientOrganizationSignUpCommand = providerClientOrganizationSignUpCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null)
|
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null)
|
||||||
@ -560,12 +564,12 @@ public class ProviderService : IProviderService
|
|||||||
|
|
||||||
ThrowOnInvalidPlanType(provider.Type, organizationSignup.Plan);
|
ThrowOnInvalidPlanType(provider.Type, organizationSignup.Plan);
|
||||||
|
|
||||||
var (organization, _, defaultCollection) = await _organizationService.SignupClientAsync(organizationSignup);
|
var signUpResponse = await _providerClientOrganizationSignUpCommand.SignUpClientOrganizationAsync(organizationSignup);
|
||||||
|
|
||||||
var providerOrganization = new ProviderOrganization
|
var providerOrganization = new ProviderOrganization
|
||||||
{
|
{
|
||||||
ProviderId = providerId,
|
ProviderId = providerId,
|
||||||
OrganizationId = organization.Id,
|
OrganizationId = signUpResponse.Organization.Id,
|
||||||
Key = organizationSignup.OwnerKey,
|
Key = organizationSignup.OwnerKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -574,12 +578,12 @@ public class ProviderService : IProviderService
|
|||||||
|
|
||||||
// Give the owner Can Manage access over the default collection
|
// Give the owner Can Manage access over the default collection
|
||||||
// The orgUser is not available when the org is created so we have to do it here as part of the invite
|
// The orgUser is not available when the org is created so we have to do it here as part of the invite
|
||||||
var defaultOwnerAccess = defaultCollection != null
|
var defaultOwnerAccess = signUpResponse.DefaultCollection != null
|
||||||
?
|
?
|
||||||
[
|
[
|
||||||
new CollectionAccessSelection
|
new CollectionAccessSelection
|
||||||
{
|
{
|
||||||
Id = defaultCollection.Id,
|
Id = signUpResponse.DefaultCollection.Id,
|
||||||
HidePasswords = false,
|
HidePasswords = false,
|
||||||
ReadOnly = false,
|
ReadOnly = false,
|
||||||
Manage = true
|
Manage = true
|
||||||
@ -587,7 +591,7 @@ public class ProviderService : IProviderService
|
|||||||
]
|
]
|
||||||
: Array.Empty<CollectionAccessSelection>();
|
: Array.Empty<CollectionAccessSelection>();
|
||||||
|
|
||||||
await _organizationService.InviteUsersAsync(organization.Id, user.Id, systemUser: null,
|
await _organizationService.InviteUsersAsync(signUpResponse.Organization.Id, user.Id, systemUser: null,
|
||||||
new (OrganizationUserInvite, string)[]
|
new (OrganizationUserInvite, string)[]
|
||||||
{
|
{
|
||||||
(
|
(
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Bit.Core.Billing.Entities;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
using CsvHelper.Configuration.Attributes;
|
using CsvHelper.Configuration.Attributes;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Billing.Models;
|
namespace Bit.Commercial.Core.Billing.Providers.Models;
|
||||||
|
|
||||||
public class ProviderClientInvoiceReportRow
|
public class ProviderClientInvoiceReportRow
|
||||||
{
|
{
|
@ -7,11 +7,12 @@ using Bit.Core.AdminConsole.Enums.Provider;
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing;
|
using Bit.Core.Billing;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -24,7 +25,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using OneOf;
|
using OneOf;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Billing;
|
namespace Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
|
|
||||||
[RequireFeature(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion)]
|
[RequireFeature(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion)]
|
||||||
public class BusinessUnitConverter(
|
public class BusinessUnitConverter(
|
||||||
@ -67,6 +68,7 @@ public class BusinessUnitConverter(
|
|||||||
organization.MaxStorageGb = updatedPlan.PasswordManager.BaseStorageGb;
|
organization.MaxStorageGb = updatedPlan.PasswordManager.BaseStorageGb;
|
||||||
organization.UsePolicies = updatedPlan.HasPolicies;
|
organization.UsePolicies = updatedPlan.HasPolicies;
|
||||||
organization.UseSso = updatedPlan.HasSso;
|
organization.UseSso = updatedPlan.HasSso;
|
||||||
|
organization.UseOrganizationDomains = updatedPlan.HasOrganizationDomains;
|
||||||
organization.UseGroups = updatedPlan.HasGroups;
|
organization.UseGroups = updatedPlan.HasGroups;
|
||||||
organization.UseEvents = updatedPlan.HasEvents;
|
organization.UseEvents = updatedPlan.HasEvents;
|
||||||
organization.UseDirectory = updatedPlan.HasDirectory;
|
organization.UseDirectory = updatedPlan.HasDirectory;
|
@ -1,5 +1,5 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Bit.Commercial.Core.Billing.Models;
|
using Bit.Commercial.Core.Billing.Providers.Models;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
@ -8,15 +8,17 @@ using Bit.Core.AdminConsole.Repositories;
|
|||||||
using Bit.Core.Billing;
|
using Bit.Core.Billing;
|
||||||
using Bit.Core.Billing.Caches;
|
using Bit.Core.Billing.Caches;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
using Bit.Core.Billing.Providers.Models;
|
||||||
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
using Bit.Core.Billing.Tax.Models;
|
||||||
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
using Bit.Core.Billing.Tax.Services;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
@ -25,15 +27,13 @@ using Bit.Core.Services;
|
|||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Braintree;
|
using Braintree;
|
||||||
using CsvHelper;
|
using CsvHelper;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
using static Bit.Core.Billing.Utilities;
|
using static Bit.Core.Billing.Utilities;
|
||||||
using Customer = Stripe.Customer;
|
using Customer = Stripe.Customer;
|
||||||
using Subscription = Stripe.Subscription;
|
using Subscription = Stripe.Subscription;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Billing;
|
namespace Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
|
|
||||||
public class ProviderBillingService(
|
public class ProviderBillingService(
|
||||||
IBraintreeGateway braintreeGateway,
|
IBraintreeGateway braintreeGateway,
|
||||||
@ -50,8 +50,7 @@ public class ProviderBillingService(
|
|||||||
ISetupIntentCache setupIntentCache,
|
ISetupIntentCache setupIntentCache,
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
ISubscriberService subscriberService,
|
ISubscriberService subscriberService,
|
||||||
ITaxService taxService,
|
ITaxService taxService)
|
||||||
[FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy)
|
|
||||||
: IProviderBillingService
|
: IProviderBillingService
|
||||||
{
|
{
|
||||||
public async Task AddExistingOrganization(
|
public async Task AddExistingOrganization(
|
||||||
@ -97,6 +96,7 @@ public class ProviderBillingService(
|
|||||||
organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb;
|
organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb;
|
||||||
organization.UsePolicies = plan.HasPolicies;
|
organization.UsePolicies = plan.HasPolicies;
|
||||||
organization.UseSso = plan.HasSso;
|
organization.UseSso = plan.HasSso;
|
||||||
|
organization.UseOrganizationDomains = plan.HasOrganizationDomains;
|
||||||
organization.UseGroups = plan.HasGroups;
|
organization.UseGroups = plan.HasGroups;
|
||||||
organization.UseEvents = plan.HasEvents;
|
organization.UseEvents = plan.HasEvents;
|
||||||
organization.UseDirectory = plan.HasDirectory;
|
organization.UseDirectory = plan.HasDirectory;
|
||||||
@ -125,7 +125,7 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* We have to scale the provider's seats before the ProviderOrganization
|
* We have to scale the provider's seats before the ProviderOrganization
|
||||||
* row is inserted so the added organization's seats don't get double counted.
|
* row is inserted so the added organization's seats don't get double-counted.
|
||||||
*/
|
*/
|
||||||
await ScaleSeats(provider, organization.PlanType, organization.Seats!.Value);
|
await ScaleSeats(provider, organization.PlanType, organization.Seats!.Value);
|
||||||
|
|
||||||
@ -233,7 +233,7 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
var providerCustomer = await subscriberService.GetCustomerOrThrow(provider, new CustomerGetOptions
|
var providerCustomer = await subscriberService.GetCustomerOrThrow(provider, new CustomerGetOptions
|
||||||
{
|
{
|
||||||
Expand = ["tax_ids"]
|
Expand = ["tax", "tax_ids"]
|
||||||
});
|
});
|
||||||
|
|
||||||
var providerTaxId = providerCustomer.TaxIds.FirstOrDefault();
|
var providerTaxId = providerCustomer.TaxIds.FirstOrDefault();
|
||||||
@ -281,6 +281,13 @@ public class ProviderBillingService(
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||||
|
|
||||||
|
if (setNonUSBusinessUseToReverseCharge && providerCustomer.Address is not { Country: "US" })
|
||||||
|
{
|
||||||
|
customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse;
|
||||||
|
}
|
||||||
|
|
||||||
var customer = await stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
var customer = await stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||||
|
|
||||||
organization.GatewayCustomerId = customer.Id;
|
organization.GatewayCustomerId = customer.Id;
|
||||||
@ -517,6 +524,13 @@ public class ProviderBillingService(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||||
|
|
||||||
|
if (setNonUSBusinessUseToReverseCharge && taxInfo.BillingAddressCountry != "US")
|
||||||
|
{
|
||||||
|
options.TaxExempt = StripeConstants.TaxExempt.Reverse;
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(taxInfo.TaxIdNumber))
|
if (!string.IsNullOrEmpty(taxInfo.TaxIdNumber))
|
||||||
{
|
{
|
||||||
var taxIdType = taxService.GetStripeTaxCode(
|
var taxIdType = taxService.GetStripeTaxCode(
|
||||||
@ -528,6 +542,7 @@ public class ProviderBillingService(
|
|||||||
logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.",
|
logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.",
|
||||||
taxInfo.BillingAddressCountry,
|
taxInfo.BillingAddressCountry,
|
||||||
taxInfo.TaxIdNumber);
|
taxInfo.TaxIdNumber);
|
||||||
|
|
||||||
throw new BadRequestException("billingTaxIdTypeInferenceError");
|
throw new BadRequestException("billingTaxIdTypeInferenceError");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -692,6 +707,13 @@ public class ProviderBillingService(
|
|||||||
customer.Metadata.ContainsKey(BraintreeCustomerIdKey) ||
|
customer.Metadata.ContainsKey(BraintreeCustomerIdKey) ||
|
||||||
setupIntent.IsUnverifiedBankAccount());
|
setupIntent.IsUnverifiedBankAccount());
|
||||||
|
|
||||||
|
int? trialPeriodDays = provider.Type switch
|
||||||
|
{
|
||||||
|
ProviderType.Msp when usePaymentMethod => 14,
|
||||||
|
ProviderType.BusinessUnit when usePaymentMethod => 4,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
||||||
{
|
{
|
||||||
CollectionMethod = usePaymentMethod ?
|
CollectionMethod = usePaymentMethod ?
|
||||||
@ -705,17 +727,24 @@ public class ProviderBillingService(
|
|||||||
},
|
},
|
||||||
OffSession = true,
|
OffSession = true,
|
||||||
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
|
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
|
||||||
TrialPeriodDays = usePaymentMethod ? 14 : null
|
TrialPeriodDays = trialPeriodDays
|
||||||
};
|
};
|
||||||
|
|
||||||
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
|
var setNonUSBusinessUseToReverseCharge =
|
||||||
{
|
featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||||
automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer);
|
|
||||||
}
|
if (setNonUSBusinessUseToReverseCharge)
|
||||||
else
|
|
||||||
{
|
{
|
||||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||||
}
|
}
|
||||||
|
else if (customer.HasRecognizedTaxLocation())
|
||||||
|
{
|
||||||
|
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||||
|
{
|
||||||
|
Enabled = customer.Address.Country == "US" ||
|
||||||
|
customer.TaxIds.Any()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
@ -6,7 +6,7 @@ using Bit.Core.Billing;
|
|||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Billing;
|
namespace Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
|
|
||||||
public static class ProviderPriceAdapter
|
public static class ProviderPriceAdapter
|
||||||
{
|
{
|
@ -1,13 +1,9 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Enums;
|
|
||||||
using Bit.Core.Billing.Licenses;
|
|
||||||
using Bit.Core.Billing.Licenses.Extensions;
|
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
namespace Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
||||||
@ -17,72 +13,42 @@ public class MaxProjectsQuery : IMaxProjectsQuery
|
|||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IProjectRepository _projectRepository;
|
private readonly IProjectRepository _projectRepository;
|
||||||
private readonly IGlobalSettings _globalSettings;
|
private readonly IGlobalSettings _globalSettings;
|
||||||
private readonly ILicensingService _licensingService;
|
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
|
|
||||||
public MaxProjectsQuery(
|
public MaxProjectsQuery(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IProjectRepository projectRepository,
|
IProjectRepository projectRepository,
|
||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
ILicensingService licensingService,
|
|
||||||
IPricingClient pricingClient)
|
IPricingClient pricingClient)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_projectRepository = projectRepository;
|
_projectRepository = projectRepository;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_licensingService = licensingService;
|
|
||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(short? max, bool? overMax)> GetByOrgIdAsync(Guid organizationId, int projectsToAdd)
|
public async Task<(short? max, bool? overMax)> GetByOrgIdAsync(Guid organizationId, int projectsToAdd)
|
||||||
{
|
{
|
||||||
|
// "MaxProjects" only applies to free 2-person organizations, which can't be self-hosted.
|
||||||
|
if (_globalSettings.SelfHosted)
|
||||||
|
{
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||||
if (org == null)
|
if (org == null)
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var (planType, maxProjects) = await GetPlanTypeAndMaxProjectsAsync(org);
|
var plan = await _pricingClient.GetPlan(org.PlanType);
|
||||||
|
|
||||||
if (planType != PlanType.Free)
|
if (plan is not { SecretsManager: not null, Type: PlanType.Free })
|
||||||
{
|
{
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var projects = await _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId);
|
var projects = await _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId);
|
||||||
return ((short? max, bool? overMax))(projects + projectsToAdd > maxProjects ? (maxProjects, true) : (maxProjects, false));
|
return ((short? max, bool? overMax))(projects + projectsToAdd > plan.SecretsManager.MaxProjects ? (plan.SecretsManager.MaxProjects, true) : (plan.SecretsManager.MaxProjects, false));
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<(PlanType planType, int maxProjects)> GetPlanTypeAndMaxProjectsAsync(Organization organization)
|
|
||||||
{
|
|
||||||
if (_globalSettings.SelfHosted)
|
|
||||||
{
|
|
||||||
var license = await _licensingService.ReadOrganizationLicenseAsync(organization);
|
|
||||||
|
|
||||||
if (license == null)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("License not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license);
|
|
||||||
var maxProjects = claimsPrincipal.GetValue<int?>(OrganizationLicenseConstants.SmMaxProjects);
|
|
||||||
|
|
||||||
if (!maxProjects.HasValue)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("License does not contain a value for max Secrets Manager projects");
|
|
||||||
}
|
|
||||||
|
|
||||||
var planType = claimsPrincipal.GetValue<PlanType>(OrganizationLicenseConstants.PlanType);
|
|
||||||
return (planType, maxProjects.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
var plan = await _pricingClient.GetPlan(organization.PlanType);
|
|
||||||
|
|
||||||
if (plan is { SupportsSecretsManager: true })
|
|
||||||
{
|
|
||||||
return (plan.Type, plan.SecretsManager.MaxProjects);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new BadRequestException("Existing plan not found.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
using Bit.Commercial.Core.AdminConsole.Providers;
|
using Bit.Commercial.Core.AdminConsole.Providers;
|
||||||
using Bit.Commercial.Core.AdminConsole.Services;
|
using Bit.Commercial.Core.AdminConsole.Services;
|
||||||
using Bit.Commercial.Core.Billing;
|
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Utilities;
|
namespace Bit.Commercial.Core.Utilities;
|
||||||
|
@ -6,10 +6,10 @@ using Bit.Core.AdminConsole.Models.Business;
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
|
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Commands;
|
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -76,9 +76,8 @@ public class PostUserCommand(
|
|||||||
var invitedOrganizationUserId = result switch
|
var invitedOrganizationUserId = result switch
|
||||||
{
|
{
|
||||||
Success<ScimInviteOrganizationUsersResponse> success => success.Value.InvitedUser.Id,
|
Success<ScimInviteOrganizationUsersResponse> success => success.Value.InvitedUser.Id,
|
||||||
Failure<ScimInviteOrganizationUsersResponse> failure when failure.Errors
|
Failure<ScimInviteOrganizationUsersResponse> { Error.Message: NoUsersToInviteError.Code } => (Guid?)null,
|
||||||
.Any(x => x.Message == NoUsersToInviteError.Code) => (Guid?)null,
|
Failure<ScimInviteOrganizationUsersResponse> failure => throw MapToBitException(failure.Error),
|
||||||
Failure<ScimInviteOrganizationUsersResponse> failure when failure.Errors.Length != 0 => throw MapToBitException(failure.Errors),
|
|
||||||
_ => throw new InvalidOperationException()
|
_ => throw new InvalidOperationException()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
8
bitwarden_license/src/Sso/package-lock.json
generated
8
bitwarden_license/src/Sso/package-lock.json
generated
@ -15,7 +15,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"css-loader": "7.1.2",
|
"css-loader": "7.1.2",
|
||||||
"expose-loader": "5.0.0",
|
"expose-loader": "5.0.1",
|
||||||
"mini-css-extract-plugin": "2.9.2",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"sass": "1.85.0",
|
"sass": "1.85.0",
|
||||||
"sass-loader": "16.0.4",
|
"sass-loader": "16.0.4",
|
||||||
@ -1083,9 +1083,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expose-loader": {
|
"node_modules/expose-loader": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.1.tgz",
|
||||||
"integrity": "sha512-BtUqYRmvx1bEY5HN6eK2I9URUZgNmN0x5UANuocaNjXSgfoDlkXt+wyEMe7i5DzDNh2BKJHPc5F4rBwEdSQX6w==",
|
"integrity": "sha512-5YPZuszN/eWND/B+xuq5nIpb/l5TV1HYmdO6SubYtHv+HenVw9/6bn33Mm5reY8DNid7AVtbARvyUD34edfCtg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"css-loader": "7.1.2",
|
"css-loader": "7.1.2",
|
||||||
"expose-loader": "5.0.0",
|
"expose-loader": "5.0.1",
|
||||||
"mini-css-extract-plugin": "2.9.2",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"sass": "1.85.0",
|
"sass": "1.85.0",
|
||||||
"sass-loader": "16.0.4",
|
"sass-loader": "16.0.4",
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Commercial.Core.AdminConsole.Providers;
|
using Bit.Commercial.Core.AdminConsole.Providers;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
@ -7,6 +8,7 @@ using Bit.Core.AdminConsole.Repositories;
|
|||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -223,31 +225,115 @@ public class RemoveOrganizationFromProviderCommandTests
|
|||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
|
||||||
|
stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
|
||||||
|
options.Description == string.Empty &&
|
||||||
|
options.Email == organization.BillingEmail &&
|
||||||
|
options.Expand[0] == "tax" &&
|
||||||
|
options.Expand[1] == "tax_ids")).Returns(new Customer
|
||||||
|
{
|
||||||
|
Id = "customer_id",
|
||||||
|
Address = new Address
|
||||||
|
{
|
||||||
|
Country = "US"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
|
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
|
||||||
{
|
{
|
||||||
Id = "subscription_id"
|
Id = "subscription_id"
|
||||||
});
|
});
|
||||||
|
|
||||||
sutProvider.GetDependency<IAutomaticTaxStrategy>()
|
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||||
.When(x => x.SetCreateOptions(
|
|
||||||
Arg.Is<SubscriptionCreateOptions>(options =>
|
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
||||||
options.Customer == organization.GatewayCustomerId &&
|
options.Customer == organization.GatewayCustomerId &&
|
||||||
options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
|
options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
|
||||||
options.DaysUntilDue == 30 &&
|
options.DaysUntilDue == 30 &&
|
||||||
options.Metadata["organizationId"] == organization.Id.ToString() &&
|
options.AutomaticTax.Enabled == true &&
|
||||||
options.OffSession == true &&
|
options.Metadata["organizationId"] == organization.Id.ToString() &&
|
||||||
options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&
|
options.OffSession == true &&
|
||||||
options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId &&
|
options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&
|
||||||
options.Items.First().Quantity == organization.Seats)
|
options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId &&
|
||||||
, Arg.Any<Customer>()))
|
options.Items.First().Quantity == organization.Seats));
|
||||||
.Do(x =>
|
|
||||||
|
await sutProvider.GetDependency<IProviderBillingService>().Received(1)
|
||||||
|
.ScaleSeats(provider, organization.PlanType, -organization.Seats ?? 0);
|
||||||
|
|
||||||
|
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(
|
||||||
|
org =>
|
||||||
|
org.BillingEmail == "a@example.com" &&
|
||||||
|
org.GatewaySubscriptionId == "subscription_id" &&
|
||||||
|
org.Status == OrganizationStatusType.Created));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
|
||||||
|
.DeleteAsync(providerOrganization);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||||
|
.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IMailService>().Received(1)
|
||||||
|
.SendProviderUpdatePaymentMethod(
|
||||||
|
organization.Id,
|
||||||
|
organization.Name,
|
||||||
|
provider.Name,
|
||||||
|
Arg.Is<IEnumerable<string>>(emails => emails.FirstOrDefault() == "a@example.com"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RemoveOrganizationFromProvider_OrganizationStripeEnabled_ConsolidatedBilling_ReverseCharge_MakesCorrectInvocations(
|
||||||
|
Provider provider,
|
||||||
|
ProviderOrganization providerOrganization,
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Status = ProviderStatusType.Billable;
|
||||||
|
|
||||||
|
providerOrganization.ProviderId = provider.Id;
|
||||||
|
|
||||||
|
organization.Status = OrganizationStatusType.Managed;
|
||||||
|
|
||||||
|
organization.PlanType = PlanType.TeamsMonthly;
|
||||||
|
|
||||||
|
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(
|
||||||
|
providerOrganization.OrganizationId,
|
||||||
|
[],
|
||||||
|
includeProvider: false)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
|
||||||
|
organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns([
|
||||||
|
"a@example.com",
|
||||||
|
"b@example.com"
|
||||||
|
]);
|
||||||
|
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
|
||||||
|
stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
|
||||||
|
options.Description == string.Empty &&
|
||||||
|
options.Email == organization.BillingEmail &&
|
||||||
|
options.Expand[0] == "tax" &&
|
||||||
|
options.Expand[1] == "tax_ids")).Returns(new Customer
|
||||||
{
|
{
|
||||||
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
|
Id = "customer_id",
|
||||||
|
Address = new Address
|
||||||
{
|
{
|
||||||
Enabled = true
|
Country = "US"
|
||||||
};
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
|
||||||
|
{
|
||||||
|
Id = "subscription_id"
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
||||||
|
|
||||||
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||||
|
|
||||||
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
||||||
|
@ -6,11 +6,12 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Business.Provider;
|
using Bit.Core.AdminConsole.Models.Business.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -717,8 +718,8 @@ public class ProviderServiceTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
|
||||||
.Returns((organization, null as OrganizationUser, new Collection()));
|
.Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));
|
||||||
|
|
||||||
var providerOrganization =
|
var providerOrganization =
|
||||||
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
|
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
|
||||||
@ -755,8 +756,8 @@ public class ProviderServiceTests
|
|||||||
|
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
|
||||||
.Returns((organization, null as OrganizationUser, new Collection()));
|
.Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));
|
||||||
|
|
||||||
await Assert.ThrowsAsync<BadRequestException>(() =>
|
await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user));
|
sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user));
|
||||||
@ -782,8 +783,8 @@ public class ProviderServiceTests
|
|||||||
|
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
|
||||||
.Returns((organization, null as OrganizationUser, new Collection()));
|
.Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));
|
||||||
|
|
||||||
var providerOrganization = await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
|
var providerOrganization = await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
|
||||||
|
|
||||||
@ -821,8 +822,8 @@ public class ProviderServiceTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
|
||||||
.Returns((organization, null as OrganizationUser, defaultCollection));
|
.Returns(new ProviderClientOrganizationSignUpResponse(organization, defaultCollection));
|
||||||
|
|
||||||
var providerOrganization =
|
var providerOrganization =
|
||||||
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
|
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Bit.Commercial.Core.Billing;
|
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing;
|
using Bit.Core.Billing;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -25,7 +25,7 @@ using NSubstitute;
|
|||||||
using Stripe;
|
using Stripe;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Test.Billing;
|
namespace Bit.Commercial.Core.Test.Billing.Providers;
|
||||||
|
|
||||||
public class BusinessUnitConverterTests
|
public class BusinessUnitConverterTests
|
||||||
{
|
{
|
@ -1,7 +1,7 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using Bit.Commercial.Core.Billing;
|
using Bit.Commercial.Core.Billing.Providers.Models;
|
||||||
using Bit.Commercial.Core.Billing.Models;
|
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
@ -10,13 +10,14 @@ using Bit.Core.AdminConsole.Models.Data.Provider;
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Caches;
|
using Bit.Core.Billing.Caches;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
using Bit.Core.Billing.Providers.Models;
|
||||||
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
using Bit.Core.Billing.Tax.Services;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -39,7 +40,7 @@ using Customer = Stripe.Customer;
|
|||||||
using PaymentMethod = Stripe.PaymentMethod;
|
using PaymentMethod = Stripe.PaymentMethod;
|
||||||
using Subscription = Stripe.Subscription;
|
using Subscription = Stripe.Subscription;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Test.Billing;
|
namespace Bit.Commercial.Core.Test.Billing.Providers;
|
||||||
|
|
||||||
[SutProviderCustomize]
|
[SutProviderCustomize]
|
||||||
public class ProviderBillingServiceTests
|
public class ProviderBillingServiceTests
|
||||||
@ -261,7 +262,7 @@ public class ProviderBillingServiceTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider, Arg.Is<CustomerGetOptions>(
|
sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider, Arg.Is<CustomerGetOptions>(
|
||||||
options => options.Expand.FirstOrDefault() == "tax_ids"))
|
options => options.Expand.Contains("tax") && options.Expand.Contains("tax_ids")))
|
||||||
.Returns(providerCustomer);
|
.Returns(providerCustomer);
|
||||||
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri
|
sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri
|
||||||
@ -311,6 +312,91 @@ public class ProviderBillingServiceTests
|
|||||||
org => org.GatewayCustomerId == "customer_id"));
|
org => org.GatewayCustomerId == "customer_id"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task CreateCustomer_ForClientOrg_ReverseCharge_Succeeds(
|
||||||
|
Provider provider,
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
|
{
|
||||||
|
organization.GatewayCustomerId = null;
|
||||||
|
organization.Name = "Name";
|
||||||
|
organization.BusinessName = "BusinessName";
|
||||||
|
|
||||||
|
var providerCustomer = new Customer
|
||||||
|
{
|
||||||
|
Address = new Address
|
||||||
|
{
|
||||||
|
Country = "CA",
|
||||||
|
PostalCode = "12345",
|
||||||
|
Line1 = "123 Main St.",
|
||||||
|
Line2 = "Unit 4",
|
||||||
|
City = "Fake Town",
|
||||||
|
State = "Fake State"
|
||||||
|
},
|
||||||
|
TaxIds = new StripeList<TaxId>
|
||||||
|
{
|
||||||
|
Data =
|
||||||
|
[
|
||||||
|
new TaxId { Type = "TYPE", Value = "VALUE" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider, Arg.Is<CustomerGetOptions>(
|
||||||
|
options => options.Expand.Contains("tax") && options.Expand.Contains("tax_ids")))
|
||||||
|
.Returns(providerCustomer);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri
|
||||||
|
.Returns(new Bit.Core.Settings.GlobalSettings.BaseServiceUriSettings(new Bit.Core.Settings.GlobalSettings())
|
||||||
|
{
|
||||||
|
CloudRegion = "US"
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(
|
||||||
|
options =>
|
||||||
|
options.Address.Country == providerCustomer.Address.Country &&
|
||||||
|
options.Address.PostalCode == providerCustomer.Address.PostalCode &&
|
||||||
|
options.Address.Line1 == providerCustomer.Address.Line1 &&
|
||||||
|
options.Address.Line2 == providerCustomer.Address.Line2 &&
|
||||||
|
options.Address.City == providerCustomer.Address.City &&
|
||||||
|
options.Address.State == providerCustomer.Address.State &&
|
||||||
|
options.Name == organization.DisplayName() &&
|
||||||
|
options.Description == $"{provider.Name} Client Organization" &&
|
||||||
|
options.Email == provider.BillingEmail &&
|
||||||
|
options.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Organization" &&
|
||||||
|
options.InvoiceSettings.CustomFields.FirstOrDefault().Value == "Name" &&
|
||||||
|
options.Metadata["region"] == "US" &&
|
||||||
|
options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type &&
|
||||||
|
options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value &&
|
||||||
|
options.TaxExempt == StripeConstants.TaxExempt.Reverse))
|
||||||
|
.Returns(new Customer { Id = "customer_id" });
|
||||||
|
|
||||||
|
await sutProvider.Sut.CreateCustomerForClientOrganization(provider, organization);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(
|
||||||
|
options =>
|
||||||
|
options.Address.Country == providerCustomer.Address.Country &&
|
||||||
|
options.Address.PostalCode == providerCustomer.Address.PostalCode &&
|
||||||
|
options.Address.Line1 == providerCustomer.Address.Line1 &&
|
||||||
|
options.Address.Line2 == providerCustomer.Address.Line2 &&
|
||||||
|
options.Address.City == providerCustomer.Address.City &&
|
||||||
|
options.Address.State == providerCustomer.Address.State &&
|
||||||
|
options.Name == organization.DisplayName() &&
|
||||||
|
options.Description == $"{provider.Name} Client Organization" &&
|
||||||
|
options.Email == provider.BillingEmail &&
|
||||||
|
options.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Organization" &&
|
||||||
|
options.InvoiceSettings.CustomFields.FirstOrDefault().Value == "Name" &&
|
||||||
|
options.Metadata["region"] == "US" &&
|
||||||
|
options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type &&
|
||||||
|
options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(Arg.Is<Organization>(
|
||||||
|
org => org.GatewayCustomerId == "customer_id"));
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region GenerateClientInvoiceReport
|
#region GenerateClientInvoiceReport
|
||||||
@ -1181,6 +1267,62 @@ public class ProviderBillingServiceTests
|
|||||||
Assert.Equivalent(expected, actual);
|
Assert.Equivalent(expected, actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SetupCustomer_WithCard_ReverseCharge_Success(
|
||||||
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
|
Provider provider,
|
||||||
|
TaxInfo taxInfo)
|
||||||
|
{
|
||||||
|
provider.Name = "MSP";
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ITaxService>()
|
||||||
|
.GetStripeTaxCode(Arg.Is<string>(
|
||||||
|
p => p == taxInfo.BillingAddressCountry),
|
||||||
|
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||||
|
.Returns(taxInfo.TaxIdType);
|
||||||
|
|
||||||
|
taxInfo.BillingAddressCountry = "AD";
|
||||||
|
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
|
||||||
|
var expected = new Customer
|
||||||
|
{
|
||||||
|
Id = "customer_id",
|
||||||
|
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||||
|
};
|
||||||
|
|
||||||
|
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token");
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
||||||
|
|
||||||
|
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||||
|
o.Address.Country == taxInfo.BillingAddressCountry &&
|
||||||
|
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
||||||
|
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
||||||
|
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
||||||
|
o.Address.City == taxInfo.BillingAddressCity &&
|
||||||
|
o.Address.State == taxInfo.BillingAddressState &&
|
||||||
|
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
|
||||||
|
o.Email == provider.BillingEmail &&
|
||||||
|
o.PaymentMethod == tokenizedPaymentSource.Token &&
|
||||||
|
o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentSource.Token &&
|
||||||
|
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
|
||||||
|
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
|
||||||
|
o.Metadata["region"] == "" &&
|
||||||
|
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
|
||||||
|
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber &&
|
||||||
|
o.TaxExempt == StripeConstants.TaxExempt.Reverse))
|
||||||
|
.Returns(expected);
|
||||||
|
|
||||||
|
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
|
||||||
|
|
||||||
|
Assert.Equivalent(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid(
|
public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid(
|
||||||
SutProvider<ProviderBillingService> sutProvider,
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
@ -1306,7 +1448,7 @@ public class ProviderBillingServiceTests
|
|||||||
.Returns(new Customer
|
.Returns(new Customer
|
||||||
{
|
{
|
||||||
Id = "customer_id",
|
Id = "customer_id",
|
||||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
Address = new Address { Country = "US" }
|
||||||
});
|
});
|
||||||
|
|
||||||
var providerPlans = new List<ProviderPlan>
|
var providerPlans = new List<ProviderPlan>
|
||||||
@ -1358,7 +1500,7 @@ public class ProviderBillingServiceTests
|
|||||||
var customer = new Customer
|
var customer = new Customer
|
||||||
{
|
{
|
||||||
Id = "customer_id",
|
Id = "customer_id",
|
||||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
Address = new Address { Country = "US" }
|
||||||
};
|
};
|
||||||
sutProvider.GetDependency<ISubscriberService>()
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
.GetCustomerOrThrow(
|
.GetCustomerOrThrow(
|
||||||
@ -1398,19 +1540,6 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||||
|
|
||||||
sutProvider.GetDependency<IAutomaticTaxStrategy>()
|
|
||||||
.When(x => x.SetCreateOptions(
|
|
||||||
Arg.Is<SubscriptionCreateOptions>(options =>
|
|
||||||
options.Customer == "customer_id")
|
|
||||||
, Arg.Is<Customer>(p => p == customer)))
|
|
||||||
.Do(x =>
|
|
||||||
{
|
|
||||||
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
|
|
||||||
{
|
|
||||||
Enabled = true
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||||
sub =>
|
sub =>
|
||||||
sub.AutomaticTax.Enabled == true &&
|
sub.AutomaticTax.Enabled == true &&
|
||||||
@ -1442,11 +1571,11 @@ public class ProviderBillingServiceTests
|
|||||||
var customer = new Customer
|
var customer = new Customer
|
||||||
{
|
{
|
||||||
Id = "customer_id",
|
Id = "customer_id",
|
||||||
|
Address = new Address { Country = "US" },
|
||||||
InvoiceSettings = new CustomerInvoiceSettings
|
InvoiceSettings = new CustomerInvoiceSettings
|
||||||
{
|
{
|
||||||
DefaultPaymentMethodId = "pm_123"
|
DefaultPaymentMethodId = "pm_123"
|
||||||
},
|
}
|
||||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<ISubscriberService>()
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
@ -1487,19 +1616,6 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||||
|
|
||||||
sutProvider.GetDependency<IAutomaticTaxStrategy>()
|
|
||||||
.When(x => x.SetCreateOptions(
|
|
||||||
Arg.Is<SubscriptionCreateOptions>(options =>
|
|
||||||
options.Customer == "customer_id")
|
|
||||||
, Arg.Is<Customer>(p => p == customer)))
|
|
||||||
.Do(x =>
|
|
||||||
{
|
|
||||||
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
|
|
||||||
{
|
|
||||||
Enabled = true
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||||
|
|
||||||
@ -1535,9 +1651,9 @@ public class ProviderBillingServiceTests
|
|||||||
var customer = new Customer
|
var customer = new Customer
|
||||||
{
|
{
|
||||||
Id = "customer_id",
|
Id = "customer_id",
|
||||||
|
Address = new Address { Country = "US" },
|
||||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||||
Metadata = new Dictionary<string, string>(),
|
Metadata = new Dictionary<string, string>()
|
||||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<ISubscriberService>()
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
@ -1578,19 +1694,6 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||||
|
|
||||||
sutProvider.GetDependency<IAutomaticTaxStrategy>()
|
|
||||||
.When(x => x.SetCreateOptions(
|
|
||||||
Arg.Is<SubscriptionCreateOptions>(options =>
|
|
||||||
options.Customer == "customer_id")
|
|
||||||
, Arg.Is<Customer>(p => p == customer)))
|
|
||||||
.Do(x =>
|
|
||||||
{
|
|
||||||
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
|
|
||||||
{
|
|
||||||
Enabled = true
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||||
|
|
||||||
@ -1645,12 +1748,15 @@ public class ProviderBillingServiceTests
|
|||||||
var customer = new Customer
|
var customer = new Customer
|
||||||
{
|
{
|
||||||
Id = "customer_id",
|
Id = "customer_id",
|
||||||
|
Address = new Address
|
||||||
|
{
|
||||||
|
Country = "US"
|
||||||
|
},
|
||||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||||
Metadata = new Dictionary<string, string>
|
Metadata = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["btCustomerId"] = "braintree_customer_id"
|
["btCustomerId"] = "braintree_customer_id"
|
||||||
},
|
}
|
||||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<ISubscriberService>()
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
@ -1691,22 +1797,92 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||||
|
|
||||||
sutProvider.GetDependency<IAutomaticTaxStrategy>()
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
.When(x => x.SetCreateOptions(
|
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||||
Arg.Is<SubscriptionCreateOptions>(options =>
|
|
||||||
options.Customer == "customer_id")
|
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||||
, Arg.Is<Customer>(p => p == customer)))
|
sub =>
|
||||||
.Do(x =>
|
sub.AutomaticTax.Enabled == true &&
|
||||||
|
sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically &&
|
||||||
|
sub.Customer == "customer_id" &&
|
||||||
|
sub.DaysUntilDue == null &&
|
||||||
|
sub.Items.Count == 2 &&
|
||||||
|
sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams &&
|
||||||
|
sub.Items.ElementAt(0).Quantity == 100 &&
|
||||||
|
sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise &&
|
||||||
|
sub.Items.ElementAt(1).Quantity == 100 &&
|
||||||
|
sub.Metadata["providerId"] == provider.Id.ToString() &&
|
||||||
|
sub.OffSession == true &&
|
||||||
|
sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&
|
||||||
|
sub.TrialPeriodDays == 14)).Returns(expected);
|
||||||
|
|
||||||
|
var actual = await sutProvider.Sut.SetupSubscription(provider);
|
||||||
|
|
||||||
|
Assert.Equivalent(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SetupSubscription_ReverseCharge_Succeeds(
|
||||||
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
|
Provider provider)
|
||||||
|
{
|
||||||
|
provider.Type = ProviderType.Msp;
|
||||||
|
provider.GatewaySubscriptionId = null;
|
||||||
|
|
||||||
|
var customer = new Customer
|
||||||
|
{
|
||||||
|
Id = "customer_id",
|
||||||
|
Address = new Address { Country = "CA" },
|
||||||
|
InvoiceSettings = new CustomerInvoiceSettings
|
||||||
{
|
{
|
||||||
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
|
DefaultPaymentMethodId = "pm_123"
|
||||||
{
|
}
|
||||||
Enabled = true
|
};
|
||||||
};
|
|
||||||
});
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetCustomerOrThrow(
|
||||||
|
provider,
|
||||||
|
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))).Returns(customer);
|
||||||
|
|
||||||
|
var providerPlans = new List<ProviderPlan>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
ProviderId = provider.Id,
|
||||||
|
PlanType = PlanType.TeamsMonthly,
|
||||||
|
SeatMinimum = 100,
|
||||||
|
PurchasedSeats = 0,
|
||||||
|
AllocatedSeats = 0
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
ProviderId = provider.Id,
|
||||||
|
PlanType = PlanType.EnterpriseMonthly,
|
||||||
|
SeatMinimum = 100,
|
||||||
|
PurchasedSeats = 0,
|
||||||
|
AllocatedSeats = 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var plan in providerPlans)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||||
|
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||||
|
}
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
|
||||||
|
.Returns(providerPlans);
|
||||||
|
|
||||||
|
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
||||||
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||||
sub =>
|
sub =>
|
||||||
sub.AutomaticTax.Enabled == true &&
|
sub.AutomaticTax.Enabled == true &&
|
@ -1,11 +1,11 @@
|
|||||||
using Bit.Commercial.Core.Billing;
|
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Test.Billing;
|
namespace Bit.Commercial.Core.Test.Billing.Providers;
|
||||||
|
|
||||||
public class ProviderPriceAdapterTests
|
public class ProviderPriceAdapterTests
|
||||||
{
|
{
|
@ -1,9 +1,9 @@
|
|||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Test.Billing;
|
namespace Bit.Commercial.Core.Test.Billing.Tax;
|
||||||
|
|
||||||
[SutProviderCustomize]
|
[SutProviderCustomize]
|
||||||
public class TaxServiceTests
|
public class TaxServiceTests
|
@ -1,14 +1,10 @@
|
|||||||
using System.Security.Claims;
|
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
||||||
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Licenses;
|
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Business;
|
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
@ -22,11 +18,26 @@ namespace Bit.Commercial.Core.Test.SecretsManager.Queries.Projects;
|
|||||||
[SutProviderCustomize]
|
[SutProviderCustomize]
|
||||||
public class MaxProjectsQueryTests
|
public class MaxProjectsQueryTests
|
||||||
{
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task GetByOrgIdAsync_SelfHosted_ReturnsNulls(SutProvider<MaxProjectsQuery> sutProvider,
|
||||||
|
Guid organizationId)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
|
||||||
|
|
||||||
|
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1);
|
||||||
|
|
||||||
|
Assert.Null(max);
|
||||||
|
Assert.Null(overMax);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task GetByOrgIdAsync_OrganizationIsNull_ThrowsNotFound(SutProvider<MaxProjectsQuery> sutProvider,
|
public async Task GetByOrgIdAsync_OrganizationIsNull_ThrowsNotFound(SutProvider<MaxProjectsQuery> sutProvider,
|
||||||
Guid organizationId)
|
Guid organizationId)
|
||||||
{
|
{
|
||||||
|
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(default).ReturnsNull();
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(default).ReturnsNull();
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1));
|
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1));
|
||||||
@ -35,54 +46,6 @@ public class MaxProjectsQueryTests
|
|||||||
.GetProjectCountByOrganizationIdAsync(organizationId);
|
.GetProjectCountByOrganizationIdAsync(organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData(PlanType.FamiliesAnnually2019)]
|
|
||||||
[BitAutoData(PlanType.Custom)]
|
|
||||||
[BitAutoData(PlanType.FamiliesAnnually)]
|
|
||||||
public async Task GetByOrgIdAsync_Cloud_SmPlanIsNull_ThrowsBadRequest(PlanType planType,
|
|
||||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
|
||||||
{
|
|
||||||
organization.PlanType = planType;
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>()
|
|
||||||
.GetByIdAsync(organization.Id)
|
|
||||||
.Returns(organization);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
|
||||||
var plan = StaticStore.GetPlan(planType);
|
|
||||||
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType).Returns(plan);
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1));
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IProjectRepository>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.GetProjectCountByOrganizationIdAsync(organization.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task GetByOrgIdAsync_SelfHosted_NoMaxProjectsClaim_ThrowsBadRequest(
|
|
||||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
|
||||||
{
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>()
|
|
||||||
.GetByIdAsync(organization.Id)
|
|
||||||
.Returns(organization);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
|
|
||||||
|
|
||||||
var license = new OrganizationLicense();
|
|
||||||
var claimsPrincipal = new ClaimsPrincipal();
|
|
||||||
sutProvider.GetDependency<ILicensingService>().ReadOrganizationLicenseAsync(organization).Returns(license);
|
|
||||||
sutProvider.GetDependency<ILicensingService>().GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1));
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IProjectRepository>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.GetProjectCountByOrganizationIdAsync(organization.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData(PlanType.TeamsMonthly2019)]
|
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||||
[BitAutoData(PlanType.TeamsMonthly2020)]
|
[BitAutoData(PlanType.TeamsMonthly2020)]
|
||||||
@ -97,57 +60,16 @@ public class MaxProjectsQueryTests
|
|||||||
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
||||||
[BitAutoData(PlanType.EnterpriseAnnually2020)]
|
[BitAutoData(PlanType.EnterpriseAnnually2020)]
|
||||||
[BitAutoData(PlanType.EnterpriseAnnually)]
|
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||||
public async Task GetByOrgIdAsync_Cloud_SmNoneFreePlans_ReturnsNull(PlanType planType,
|
public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType,
|
||||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
||||||
{
|
{
|
||||||
organization.PlanType = planType;
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||||
var plan = StaticStore.GetPlan(planType);
|
|
||||||
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType).Returns(plan);
|
|
||||||
|
|
||||||
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);
|
|
||||||
|
|
||||||
Assert.Null(limit);
|
|
||||||
Assert.Null(overLimit);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs()
|
|
||||||
.GetProjectCountByOrganizationIdAsync(organization.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData(PlanType.TeamsMonthly2019)]
|
|
||||||
[BitAutoData(PlanType.TeamsMonthly2020)]
|
|
||||||
[BitAutoData(PlanType.TeamsMonthly)]
|
|
||||||
[BitAutoData(PlanType.TeamsAnnually2019)]
|
|
||||||
[BitAutoData(PlanType.TeamsAnnually2020)]
|
|
||||||
[BitAutoData(PlanType.TeamsAnnually)]
|
|
||||||
[BitAutoData(PlanType.TeamsStarter)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseMonthly2019)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseMonthly2020)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseMonthly)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseAnnually2020)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseAnnually)]
|
|
||||||
public async Task GetByOrgIdAsync_SelfHosted_SmNoneFreePlans_ReturnsNull(PlanType planType,
|
|
||||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
|
||||||
{
|
|
||||||
organization.PlanType = planType;
|
organization.PlanType = planType;
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
|
|
||||||
|
|
||||||
var license = new OrganizationLicense();
|
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
|
||||||
var plan = StaticStore.GetPlan(planType);
|
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||||
var claims = new List<Claim>
|
|
||||||
{
|
|
||||||
new (nameof(OrganizationLicenseConstants.PlanType), organization.PlanType.ToString()),
|
|
||||||
new (nameof(OrganizationLicenseConstants.SmMaxProjects), plan.SecretsManager.MaxProjects.ToString())
|
|
||||||
};
|
|
||||||
var identity = new ClaimsIdentity(claims, "TestAuthenticationType");
|
|
||||||
var claimsPrincipal = new ClaimsPrincipal(identity);
|
|
||||||
sutProvider.GetDependency<ILicensingService>().ReadOrganizationLicenseAsync(organization).Returns(license);
|
|
||||||
sutProvider.GetDependency<ILicensingService>().GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);
|
|
||||||
|
|
||||||
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);
|
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);
|
||||||
|
|
||||||
@ -183,7 +105,7 @@ public class MaxProjectsQueryTests
|
|||||||
[BitAutoData(PlanType.Free, 3, 4, true)]
|
[BitAutoData(PlanType.Free, 3, 4, true)]
|
||||||
[BitAutoData(PlanType.Free, 4, 4, true)]
|
[BitAutoData(PlanType.Free, 4, 4, true)]
|
||||||
[BitAutoData(PlanType.Free, 40, 4, true)]
|
[BitAutoData(PlanType.Free, 40, 4, true)]
|
||||||
public async Task GetByOrgIdAsync_Cloud_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax,
|
public async Task GetByOrgIdAsync_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax,
|
||||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
||||||
{
|
{
|
||||||
organization.PlanType = planType;
|
organization.PlanType = planType;
|
||||||
@ -191,66 +113,8 @@ public class MaxProjectsQueryTests
|
|||||||
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
|
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
|
||||||
.Returns(projects);
|
.Returns(projects);
|
||||||
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
|
||||||
var plan = StaticStore.GetPlan(planType);
|
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||||
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType).Returns(plan);
|
|
||||||
|
|
||||||
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);
|
|
||||||
|
|
||||||
Assert.NotNull(max);
|
|
||||||
Assert.NotNull(overMax);
|
|
||||||
Assert.Equal(3, max.Value);
|
|
||||||
Assert.Equal(expectedOverMax, overMax);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IProjectRepository>().Received(1)
|
|
||||||
.GetProjectCountByOrganizationIdAsync(organization.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData(PlanType.Free, 0, 1, false)]
|
|
||||||
[BitAutoData(PlanType.Free, 1, 1, false)]
|
|
||||||
[BitAutoData(PlanType.Free, 2, 1, false)]
|
|
||||||
[BitAutoData(PlanType.Free, 3, 1, true)]
|
|
||||||
[BitAutoData(PlanType.Free, 4, 1, true)]
|
|
||||||
[BitAutoData(PlanType.Free, 40, 1, true)]
|
|
||||||
[BitAutoData(PlanType.Free, 0, 2, false)]
|
|
||||||
[BitAutoData(PlanType.Free, 1, 2, false)]
|
|
||||||
[BitAutoData(PlanType.Free, 2, 2, true)]
|
|
||||||
[BitAutoData(PlanType.Free, 3, 2, true)]
|
|
||||||
[BitAutoData(PlanType.Free, 4, 2, true)]
|
|
||||||
[BitAutoData(PlanType.Free, 40, 2, true)]
|
|
||||||
[BitAutoData(PlanType.Free, 0, 3, false)]
|
|
||||||
[BitAutoData(PlanType.Free, 1, 3, true)]
|
|
||||||
[BitAutoData(PlanType.Free, 2, 3, true)]
|
|
||||||
[BitAutoData(PlanType.Free, 3, 3, true)]
|
|
||||||
[BitAutoData(PlanType.Free, 4, 3, true)]
|
|
||||||
[BitAutoData(PlanType.Free, 40, 3, true)]
|
|
||||||
[BitAutoData(PlanType.Free, 0, 4, true)]
|
|
||||||
[BitAutoData(PlanType.Free, 1, 4, true)]
|
|
||||||
[BitAutoData(PlanType.Free, 2, 4, true)]
|
|
||||||
[BitAutoData(PlanType.Free, 3, 4, true)]
|
|
||||||
[BitAutoData(PlanType.Free, 4, 4, true)]
|
|
||||||
[BitAutoData(PlanType.Free, 40, 4, true)]
|
|
||||||
public async Task GetByOrgIdAsync_SelfHosted_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax,
|
|
||||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
|
||||||
{
|
|
||||||
organization.PlanType = planType;
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
|
||||||
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
|
|
||||||
.Returns(projects);
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
|
|
||||||
|
|
||||||
var license = new OrganizationLicense();
|
|
||||||
var plan = StaticStore.GetPlan(planType);
|
|
||||||
var claims = new List<Claim>
|
|
||||||
{
|
|
||||||
new (nameof(OrganizationLicenseConstants.PlanType), organization.PlanType.ToString()),
|
|
||||||
new (nameof(OrganizationLicenseConstants.SmMaxProjects), plan.SecretsManager.MaxProjects.ToString())
|
|
||||||
};
|
|
||||||
var identity = new ClaimsIdentity(claims, "TestAuthenticationType");
|
|
||||||
var claimsPrincipal = new ClaimsPrincipal(identity);
|
|
||||||
sutProvider.GetDependency<ILicensingService>().ReadOrganizationLicenseAsync(organization).Returns(license);
|
|
||||||
sutProvider.GetDependency<ILicensingService>().GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);
|
|
||||||
|
|
||||||
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);
|
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ MAILCATCHER_PORT=1080
|
|||||||
# Alternative databases
|
# Alternative databases
|
||||||
POSTGRES_PASSWORD=SET_A_PASSWORD_HERE_123
|
POSTGRES_PASSWORD=SET_A_PASSWORD_HERE_123
|
||||||
MYSQL_ROOT_PASSWORD=SET_A_PASSWORD_HERE_123
|
MYSQL_ROOT_PASSWORD=SET_A_PASSWORD_HERE_123
|
||||||
|
MARIADB_ROOT_PASSWORD=SET_A_PASSWORD_HERE_123
|
||||||
|
|
||||||
# IdP configuration
|
# IdP configuration
|
||||||
# Complete using the values from the Manage SSO page in the web vault
|
# Complete using the values from the Manage SSO page in the web vault
|
||||||
|
@ -70,6 +70,20 @@ services:
|
|||||||
profiles:
|
profiles:
|
||||||
- mysql
|
- mysql
|
||||||
|
|
||||||
|
mariadb:
|
||||||
|
image: mariadb:10
|
||||||
|
ports:
|
||||||
|
- 4306:3306
|
||||||
|
environment:
|
||||||
|
MARIADB_USER: maria
|
||||||
|
MARIADB_PASSWORD: ${MARIADB_ROOT_PASSWORD}
|
||||||
|
MARIADB_DATABASE: vault_dev
|
||||||
|
MARIADB_RANDOM_ROOT_PASSWORD: "true"
|
||||||
|
volumes:
|
||||||
|
- mariadb_dev_data:/var/lib/mysql
|
||||||
|
profiles:
|
||||||
|
- mariadb
|
||||||
|
|
||||||
idp:
|
idp:
|
||||||
image: kenchan0130/simplesamlphp:1.19.8
|
image: kenchan0130/simplesamlphp:1.19.8
|
||||||
container_name: idp
|
container_name: idp
|
||||||
|
@ -5,6 +5,7 @@ param(
|
|||||||
[switch]$all,
|
[switch]$all,
|
||||||
[switch]$postgres,
|
[switch]$postgres,
|
||||||
[switch]$mysql,
|
[switch]$mysql,
|
||||||
|
[switch]$mariadb,
|
||||||
[switch]$mssql,
|
[switch]$mssql,
|
||||||
[switch]$sqlite,
|
[switch]$sqlite,
|
||||||
[switch]$selfhost,
|
[switch]$selfhost,
|
||||||
@ -15,11 +16,15 @@ param(
|
|||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$currentDir = Get-Location
|
$currentDir = Get-Location
|
||||||
|
|
||||||
if (!$all -and !$postgres -and !$mysql -and !$sqlite) {
|
function Get-IsEFDatabase {
|
||||||
|
return $postgres -or $mysql -or $mariadb -or $sqlite;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$all -and !$(Get-IsEFDatabase)) {
|
||||||
$mssql = $true;
|
$mssql = $true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($all -or $postgres -or $mysql -or $sqlite) {
|
if ($all -or $(Get-IsEFDatabase)) {
|
||||||
dotnet ef *> $null
|
dotnet ef *> $null
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
Write-Host "Entity Framework Core tools were not found in the dotnet global tools. Attempting to install"
|
Write-Host "Entity Framework Core tools were not found in the dotnet global tools. Attempting to install"
|
||||||
@ -60,9 +65,12 @@ if ($all -or $mssql) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Foreach ($item in @(
|
Foreach ($item in @(
|
||||||
@($mysql, "MySQL", "MySqlMigrations", "mySql", 2),
|
|
||||||
@($postgres, "PostgreSQL", "PostgresMigrations", "postgreSql", 0),
|
@($postgres, "PostgreSQL", "PostgresMigrations", "postgreSql", 0),
|
||||||
@($sqlite, "SQLite", "SqliteMigrations", "sqlite", 1)
|
@($sqlite, "SQLite", "SqliteMigrations", "sqlite", 1),
|
||||||
|
@($mysql, "MySQL", "MySqlMigrations", "mySql", 2),
|
||||||
|
# MariaDB shares the MySQL connection string in the server config so they are mutually exclusive in that context.
|
||||||
|
# However they can still be run independently for integration tests.
|
||||||
|
@($mariadb, "MariaDB", "MySqlMigrations", "mySql", 3)
|
||||||
)) {
|
)) {
|
||||||
if (!$item[0] -and !$all) {
|
if (!$item[0] -and !$all) {
|
||||||
continue
|
continue
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
"rollForward": "latestFeature"
|
"rollForward": "latestFeature"
|
||||||
},
|
},
|
||||||
"msbuild-sdks": {
|
"msbuild-sdks": {
|
||||||
"Microsoft.Build.Traversal": "4.1.0"
|
"Microsoft.Build.Traversal": "4.1.0",
|
||||||
|
"Microsoft.Build.Sql": "0.1.9-preview"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,8 +40,6 @@ export function authenticate(
|
|||||||
payload["deviceName"] = "chrome";
|
payload["deviceName"] = "chrome";
|
||||||
payload["username"] = username;
|
payload["username"] = username;
|
||||||
payload["password"] = password;
|
payload["password"] = password;
|
||||||
|
|
||||||
params.headers["Auth-Email"] = encoding.b64encode(username);
|
|
||||||
} else {
|
} else {
|
||||||
payload["scope"] = "api.organization";
|
payload["scope"] = "api.organization";
|
||||||
payload["grant_type"] = "client_credentials";
|
payload["grant_type"] = "client_credentials";
|
||||||
|
@ -11,7 +11,7 @@ using Bit.Core.AdminConsole.Repositories;
|
|||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.OrganizationConnectionConfigs;
|
using Bit.Core.Models.OrganizationConnectionConfigs;
|
||||||
@ -462,6 +462,7 @@ public class OrganizationsController : Controller
|
|||||||
organization.UsersGetPremium = model.UsersGetPremium;
|
organization.UsersGetPremium = model.UsersGetPremium;
|
||||||
organization.UseSecretsManager = model.UseSecretsManager;
|
organization.UseSecretsManager = model.UseSecretsManager;
|
||||||
organization.UseRiskInsights = model.UseRiskInsights;
|
organization.UseRiskInsights = model.UseRiskInsights;
|
||||||
|
organization.UseOrganizationDomains = model.UseOrganizationDomains;
|
||||||
organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies;
|
organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies;
|
||||||
|
|
||||||
//secrets
|
//secrets
|
||||||
|
@ -10,13 +10,13 @@ using Bit.Core.AdminConsole.Providers.Interfaces;
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Providers.Models;
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
@ -102,7 +102,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
|||||||
MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats;
|
MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats;
|
||||||
SmServiceAccounts = org.SmServiceAccounts;
|
SmServiceAccounts = org.SmServiceAccounts;
|
||||||
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
|
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
|
||||||
|
UseOrganizationDomains = org.UseOrganizationDomains;
|
||||||
_plans = plans;
|
_plans = plans;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,6 +186,8 @@ public class OrganizationEditModel : OrganizationViewModel
|
|||||||
public int? SmServiceAccounts { get; set; }
|
public int? SmServiceAccounts { get; set; }
|
||||||
[Display(Name = "Max Autoscale Machine Accounts")]
|
[Display(Name = "Max Autoscale Machine Accounts")]
|
||||||
public int? MaxAutoscaleSmServiceAccounts { get; set; }
|
public int? MaxAutoscaleSmServiceAccounts { get; set; }
|
||||||
|
[Display(Name = "Use Organization Domains")]
|
||||||
|
public bool UseOrganizationDomains { get; set; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Plan[] object for use in Javascript
|
* Creates a Plan[] object for use in Javascript
|
||||||
@ -215,6 +217,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
|||||||
Has2fa = p.Has2fa,
|
Has2fa = p.Has2fa,
|
||||||
HasApi = p.HasApi,
|
HasApi = p.HasApi,
|
||||||
HasSso = p.HasSso,
|
HasSso = p.HasSso,
|
||||||
|
HasOrganizationDomains = p.HasOrganizationDomains,
|
||||||
HasKeyConnector = p.HasKeyConnector,
|
HasKeyConnector = p.HasKeyConnector,
|
||||||
HasScim = p.HasScim,
|
HasScim = p.HasScim,
|
||||||
HasResetPassword = p.HasResetPassword,
|
HasResetPassword = p.HasResetPassword,
|
||||||
@ -315,6 +318,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
|||||||
existingOrganization.MaxAutoscaleSmSeats = MaxAutoscaleSmSeats;
|
existingOrganization.MaxAutoscaleSmSeats = MaxAutoscaleSmSeats;
|
||||||
existingOrganization.SmServiceAccounts = SmServiceAccounts;
|
existingOrganization.SmServiceAccounts = SmServiceAccounts;
|
||||||
existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts;
|
existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts;
|
||||||
|
existingOrganization.UseOrganizationDomains = UseOrganizationDomains;
|
||||||
return existingOrganization;
|
return existingOrganization;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.SharedWeb.Utilities;
|
using Bit.SharedWeb.Utilities;
|
||||||
|
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
|
||||||
namespace Bit.Admin.AdminConsole.Models;
|
namespace Bit.Admin.AdminConsole.Models;
|
||||||
|
|
||||||
|
@ -124,6 +124,10 @@
|
|||||||
<input type="checkbox" class="form-check-input" asp-for="UseSso" disabled='@(canEditPlan ? null : "disabled")'>
|
<input type="checkbox" class="form-check-input" asp-for="UseSso" disabled='@(canEditPlan ? null : "disabled")'>
|
||||||
<label class="form-check-label" asp-for="UseSso"></label>
|
<label class="form-check-label" asp-for="UseSso"></label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" asp-for="UseOrganizationDomains" disabled='@(canEditPlan ? null : "disabled")'>
|
||||||
|
<label class="form-check-label" asp-for="UseOrganizationDomains"></label>
|
||||||
|
</div>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input type="checkbox" class="form-check-input" asp-for="UseKeyConnector" disabled='@(canEditPlan ? null : "disabled")'>
|
<input type="checkbox" class="form-check-input" asp-for="UseKeyConnector" disabled='@(canEditPlan ? null : "disabled")'>
|
||||||
<label class="form-check-label" asp-for="UseKeyConnector"></label>
|
<label class="form-check-label" asp-for="UseKeyConnector"></label>
|
||||||
|
@ -69,6 +69,7 @@
|
|||||||
document.getElementById('@(nameof(Model.UseGroups))').checked = plan.hasGroups;
|
document.getElementById('@(nameof(Model.UseGroups))').checked = plan.hasGroups;
|
||||||
document.getElementById('@(nameof(Model.UsePolicies))').checked = plan.hasPolicies;
|
document.getElementById('@(nameof(Model.UsePolicies))').checked = plan.hasPolicies;
|
||||||
document.getElementById('@(nameof(Model.UseSso))').checked = plan.hasSso;
|
document.getElementById('@(nameof(Model.UseSso))').checked = plan.hasSso;
|
||||||
|
document.getElementById('@(nameof(Model.UseOrganizationDomains))').checked = plan.hasOrganizationDomains;
|
||||||
document.getElementById('@(nameof(Model.UseScim))').checked = plan.hasScim;
|
document.getElementById('@(nameof(Model.UseScim))').checked = plan.hasScim;
|
||||||
document.getElementById('@(nameof(Model.UseDirectory))').checked = plan.hasDirectory;
|
document.getElementById('@(nameof(Model.UseDirectory))').checked = plan.hasDirectory;
|
||||||
document.getElementById('@(nameof(Model.UseEvents))').checked = plan.hasEvents;
|
document.getElementById('@(nameof(Model.UseEvents))').checked = plan.hasEvents;
|
||||||
|
@ -7,7 +7,7 @@ using Bit.Core.AdminConsole.Entities;
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
using Bit.Admin.Billing.Models;
|
using Bit.Admin.Billing.Models;
|
||||||
using Bit.Admin.Enums;
|
using Bit.Admin.Enums;
|
||||||
using Bit.Admin.Utilities;
|
using Bit.Admin.Utilities;
|
||||||
using Bit.Core.Billing.Migration.Models;
|
using Bit.Core.Billing.Providers.Migration.Models;
|
||||||
using Bit.Core.Billing.Migration.Services;
|
using Bit.Core.Billing.Providers.Migration.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.Billing.Entities;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
|
||||||
namespace Bit.Admin.Billing.Models;
|
namespace Bit.Admin.Billing.Models;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
@using System.Text.Json
|
@using System.Text.Json
|
||||||
@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult
|
@model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Results";
|
ViewData["Title"] = "Results";
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult[]
|
@model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult[]
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Results";
|
ViewData["Title"] = "Results";
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ using Bit.Admin.Enums;
|
|||||||
using Bit.Admin.Models;
|
using Bit.Admin.Models;
|
||||||
using Bit.Admin.Services;
|
using Bit.Admin.Services;
|
||||||
using Bit.Admin.Utilities;
|
using Bit.Admin.Utilities;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -89,7 +88,7 @@ public class UsersController : Controller
|
|||||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
|
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
|
||||||
|
|
||||||
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
|
var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id);
|
||||||
return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain));
|
return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +105,7 @@ public class UsersController : Controller
|
|||||||
var billingInfo = await _paymentService.GetBillingAsync(user);
|
var billingInfo = await _paymentService.GetBillingAsync(user);
|
||||||
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
|
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
|
||||||
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
|
var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id);
|
||||||
var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id);
|
var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id);
|
||||||
|
|
||||||
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired));
|
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired));
|
||||||
@ -167,7 +166,6 @@ public class UsersController : Controller
|
|||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
[RequirePermission(Permission.User_NewDeviceException_Edit)]
|
[RequirePermission(Permission.User_NewDeviceException_Edit)]
|
||||||
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
|
|
||||||
public async Task<IActionResult> ToggleNewDeviceVerification(Guid id)
|
public async Task<IActionResult> ToggleNewDeviceVerification(Guid id)
|
||||||
{
|
{
|
||||||
var user = await _userRepository.GetByIdAsync(id);
|
var user = await _userRepository.GetByIdAsync(id);
|
||||||
@ -179,12 +177,4 @@ public class UsersController : Controller
|
|||||||
await _userService.ToggleNewDeviceVerificationException(user.Id);
|
await _userService.ToggleNewDeviceVerificationException(user.Id);
|
||||||
return RedirectToAction("Edit", new { id });
|
return RedirectToAction("Edit", new { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Feature flag to be removed in PM-14207
|
|
||||||
private async Task<bool?> AccountDeprovisioningEnabled(Guid userId)
|
|
||||||
{
|
|
||||||
return _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
|
||||||
? await _userService.IsClaimedByAnyOrganizationAsync(userId)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ public class ChargeBraintreeModel : IValidatableObject
|
|||||||
{
|
{
|
||||||
if (Id != null)
|
if (Id != null)
|
||||||
{
|
{
|
||||||
if (Id.Length != 36 || (Id[0] != 'o' && Id[0] != 'u') ||
|
if (Id.Length != 36 || (Id[0] != 'o' && Id[0] != 'u' && Id[0] != 'p') ||
|
||||||
!Guid.TryParse(Id.Substring(1, 32), out var guid))
|
!Guid.TryParse(Id.Substring(1, 32), out var guid))
|
||||||
{
|
{
|
||||||
yield return new ValidationResult("Customer Id is not a valid format.");
|
yield return new ValidationResult("Customer Id is not a valid format.");
|
||||||
|
@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Mvc.Razor;
|
|||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Bit.Admin.Services;
|
using Bit.Admin.Services;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Migration;
|
using Bit.Core.Billing.Providers.Migration;
|
||||||
|
|
||||||
#if !OSS
|
#if !OSS
|
||||||
using Bit.Commercial.Core.Utilities;
|
using Bit.Commercial.Core.Utilities;
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Jobs;
|
using Bit.Core.Jobs;
|
||||||
using Bit.Core.Tools.Repositories;
|
using Bit.Core.Tools.Repositories;
|
||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
|
|
||||||
namespace Bit.Admin.Tools.Jobs;
|
namespace Bit.Admin.Tools.Jobs;
|
||||||
@ -32,10 +32,10 @@ public class DeleteSendsJob : BaseJob
|
|||||||
}
|
}
|
||||||
using (var scope = _serviceProvider.CreateScope())
|
using (var scope = _serviceProvider.CreateScope())
|
||||||
{
|
{
|
||||||
var sendService = scope.ServiceProvider.GetRequiredService<ISendService>();
|
var nonAnonymousSendCommand = scope.ServiceProvider.GetRequiredService<INonAnonymousSendCommand>();
|
||||||
foreach (var send in sends)
|
foreach (var send in sends)
|
||||||
{
|
{
|
||||||
await sendService.DeleteSendAsync(send);
|
await nonAnonymousSendCommand.DeleteSendAsync(send);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
8
src/Admin/package-lock.json
generated
8
src/Admin/package-lock.json
generated
@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"css-loader": "7.1.2",
|
"css-loader": "7.1.2",
|
||||||
"expose-loader": "5.0.0",
|
"expose-loader": "5.0.1",
|
||||||
"mini-css-extract-plugin": "2.9.2",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"sass": "1.85.0",
|
"sass": "1.85.0",
|
||||||
"sass-loader": "16.0.4",
|
"sass-loader": "16.0.4",
|
||||||
@ -1084,9 +1084,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expose-loader": {
|
"node_modules/expose-loader": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.1.tgz",
|
||||||
"integrity": "sha512-BtUqYRmvx1bEY5HN6eK2I9URUZgNmN0x5UANuocaNjXSgfoDlkXt+wyEMe7i5DzDNh2BKJHPc5F4rBwEdSQX6w==",
|
"integrity": "sha512-5YPZuszN/eWND/B+xuq5nIpb/l5TV1HYmdO6SubYtHv+HenVw9/6bn33Mm5reY8DNid7AVtbARvyUD34edfCtg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"css-loader": "7.1.2",
|
"css-loader": "7.1.2",
|
||||||
"expose-loader": "5.0.0",
|
"expose-loader": "5.0.1",
|
||||||
"mini-css-extract-plugin": "2.9.2",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"sass": "1.85.0",
|
"sass": "1.85.0",
|
||||||
"sass-loader": "16.0.4",
|
"sass-loader": "16.0.4",
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Api.AdminConsole.Authorization.Requirements;
|
||||||
|
|
||||||
|
public class ManageAccountRecoveryRequirement : IOrganizationRequirement
|
||||||
|
{
|
||||||
|
public async Task<bool> AuthorizeAsync(
|
||||||
|
CurrentContextOrganization? organizationClaims,
|
||||||
|
Func<Task<bool>> isProviderUserForOrg)
|
||||||
|
=> organizationClaims switch
|
||||||
|
{
|
||||||
|
{ Type: OrganizationUserType.Owner } => true,
|
||||||
|
{ Type: OrganizationUserType.Admin } => true,
|
||||||
|
{ Permissions.ManageResetPassword: true } => true,
|
||||||
|
_ => await isProviderUserForOrg()
|
||||||
|
};
|
||||||
|
}
|
@ -2,13 +2,11 @@
|
|||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -137,7 +135,6 @@ public class OrganizationDomainController : Controller
|
|||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpPost("domain/sso/verified")]
|
[HttpPost("domain/sso/verified")]
|
||||||
[RequireFeature(FeatureFlagKeys.VerifiedSsoDomainEndpoint)]
|
|
||||||
public async Task<VerifiedOrganizationDomainSsoDetailsResponseModel> GetVerifiedOrgDomainSsoDetailsAsync(
|
public async Task<VerifiedOrganizationDomainSsoDetailsResponseModel> GetVerifiedOrgDomainSsoDetailsAsync(
|
||||||
[FromBody] OrganizationDomainSsoDetailsRequestModel model)
|
[FromBody] OrganizationDomainSsoDetailsRequestModel model)
|
||||||
{
|
{
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Authorization.Requirements;
|
||||||
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
using Bit.Api.Models.Request.Organizations;
|
using Bit.Api.Models.Request.Organizations;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
@ -162,6 +163,12 @@ public class OrganizationUsersController : Controller
|
|||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false)
|
public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.SeparateCustomRolePermissions))
|
||||||
|
{
|
||||||
|
return await GetvNextAsync(orgId, includeGroups, includeCollections);
|
||||||
|
}
|
||||||
|
|
||||||
var authorized = (await _authorizationService.AuthorizeAsync(
|
var authorized = (await _authorizationService.AuthorizeAsync(
|
||||||
User, new OrganizationScope(orgId), OrganizationUserUserDetailsOperations.ReadAll)).Succeeded;
|
User, new OrganizationScope(orgId), OrganizationUserUserDetailsOperations.ReadAll)).Succeeded;
|
||||||
if (!authorized)
|
if (!authorized)
|
||||||
@ -191,6 +198,37 @@ public class OrganizationUsersController : Controller
|
|||||||
return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(responses);
|
return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> GetvNextAsync(Guid orgId, bool includeGroups = false, bool includeCollections = false)
|
||||||
|
{
|
||||||
|
var request = new OrganizationUserUserDetailsQueryRequest
|
||||||
|
{
|
||||||
|
OrganizationId = orgId,
|
||||||
|
IncludeGroups = includeGroups,
|
||||||
|
IncludeCollections = includeCollections,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ((await _authorizationService.AuthorizeAsync(User, new ManageUsersRequirement())).Succeeded)
|
||||||
|
{
|
||||||
|
return GetResultListResponseModel(await _organizationUserUserDetailsQuery.Get(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((await _authorizationService.AuthorizeAsync(User, new ManageAccountRecoveryRequirement())).Succeeded)
|
||||||
|
{
|
||||||
|
return GetResultListResponseModel(await _organizationUserUserDetailsQuery.GetAccountRecoveryEnrolledUsers(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListResponseModel<OrganizationUserUserDetailsResponseModel> GetResultListResponseModel(IEnumerable<(OrganizationUserUserDetails OrgUser,
|
||||||
|
bool TwoFactorEnabled, bool ClaimedByOrganization)> results)
|
||||||
|
{
|
||||||
|
return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(results
|
||||||
|
.Select(result => new OrganizationUserUserDetailsResponseModel(result))
|
||||||
|
.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpGet("{id}/groups")]
|
[HttpGet("{id}/groups")]
|
||||||
public async Task<IEnumerable<string>> GetGroups(string orgId, string id)
|
public async Task<IEnumerable<string>> GetGroups(string orgId, string id)
|
||||||
{
|
{
|
||||||
@ -578,7 +616,6 @@ public class OrganizationUsersController : Controller
|
|||||||
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
|
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
|
||||||
[HttpDelete("{id}/delete-account")]
|
[HttpDelete("{id}/delete-account")]
|
||||||
[HttpPost("{id}/delete-account")]
|
[HttpPost("{id}/delete-account")]
|
||||||
public async Task DeleteAccount(Guid orgId, Guid id)
|
public async Task DeleteAccount(Guid orgId, Guid id)
|
||||||
@ -597,7 +634,6 @@ public class OrganizationUsersController : Controller
|
|||||||
await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
|
await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
|
||||||
[HttpDelete("delete-account")]
|
[HttpDelete("delete-account")]
|
||||||
[HttpPost("delete-account")]
|
[HttpPost("delete-account")]
|
||||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||||
@ -722,11 +758,6 @@ public class OrganizationUsersController : Controller
|
|||||||
|
|
||||||
private async Task<IDictionary<Guid, bool>> GetClaimedByOrganizationStatusAsync(Guid orgId, IEnumerable<Guid> userIds)
|
private async Task<IDictionary<Guid, bool>> GetClaimedByOrganizationStatusAsync(Guid orgId, IEnumerable<Guid> userIds)
|
||||||
{
|
{
|
||||||
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
|
||||||
{
|
|
||||||
return userIds.ToDictionary(kvp => kvp, kvp => false);
|
|
||||||
}
|
|
||||||
|
|
||||||
var usersOrganizationClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgId, userIds);
|
var usersOrganizationClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgId, userIds);
|
||||||
return usersOrganizationClaimedStatus;
|
return usersOrganizationClaimedStatus;
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ using Bit.Core.Auth.Services;
|
|||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -279,8 +279,7 @@ public class OrganizationsController : Controller
|
|||||||
throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving.");
|
throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
if ((await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id))
|
||||||
&& (await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id))
|
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Claimed user account cannot leave claiming organization. Contact your organization administrator for additional details.");
|
throw new BadRequestException("Claimed user account cannot leave claiming organization. Contact your organization administrator for additional details.");
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
using Bit.Api.AdminConsole.Models.Response.Helpers;
|
using Bit.Api.AdminConsole.Models.Response.Helpers;
|
||||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||||
@ -79,7 +78,7 @@ public class PoliciesController : Controller
|
|||||||
return new PolicyDetailResponseModel(new Policy { Type = (PolicyType)type });
|
return new PolicyDetailResponseModel(new Policy { Type = (PolicyType)type });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && policy.Type is PolicyType.SingleOrg)
|
if (policy.Type is PolicyType.SingleOrg)
|
||||||
{
|
{
|
||||||
return await policy.GetSingleOrgPolicyDetailResponseAsync(_organizationHasVerifiedDomainsQuery);
|
return await policy.GetSingleOrgPolicyDetailResponseAsync(_organizationHasVerifiedDomainsQuery);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
using Bit.Api.Billing.Models.Requests;
|
using Bit.Api.Billing.Models.Requests;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
|
@ -75,6 +75,8 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
|||||||
|
|
||||||
public string InitiationPath { get; set; }
|
public string InitiationPath { get; set; }
|
||||||
|
|
||||||
|
public bool SkipTrial { get; set; }
|
||||||
|
|
||||||
public virtual OrganizationSignup ToOrganizationSignup(User user)
|
public virtual OrganizationSignup ToOrganizationSignup(User user)
|
||||||
{
|
{
|
||||||
var orgSignup = new OrganizationSignup
|
var orgSignup = new OrganizationSignup
|
||||||
@ -107,6 +109,7 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
|||||||
BillingAddressCountry = BillingAddressCountry,
|
BillingAddressCountry = BillingAddressCountry,
|
||||||
},
|
},
|
||||||
InitiationPath = InitiationPath,
|
InitiationPath = InitiationPath,
|
||||||
|
SkipTrial = SkipTrial
|
||||||
};
|
};
|
||||||
|
|
||||||
Keys?.ToOrganizationSignup(orgSignup);
|
Keys?.ToOrganizationSignup(orgSignup);
|
||||||
|
@ -64,6 +64,7 @@ public class OrganizationResponseModel : ResponseModel
|
|||||||
LimitItemDeletion = organization.LimitItemDeletion;
|
LimitItemDeletion = organization.LimitItemDeletion;
|
||||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||||
UseRiskInsights = organization.UseRiskInsights;
|
UseRiskInsights = organization.UseRiskInsights;
|
||||||
|
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,6 +112,7 @@ public class OrganizationResponseModel : ResponseModel
|
|||||||
public bool LimitItemDeletion { get; set; }
|
public bool LimitItemDeletion { get; set; }
|
||||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||||
public bool UseRiskInsights { get; set; }
|
public bool UseRiskInsights { get; set; }
|
||||||
|
public bool UseOrganizationDomains { get; set; }
|
||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,6 +126,26 @@ public class OrganizationUserUserMiniDetailsResponseModel : ResponseModel
|
|||||||
|
|
||||||
public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel
|
public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel
|
||||||
{
|
{
|
||||||
|
public OrganizationUserUserDetailsResponseModel((OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization) data, string obj = "organizationUserUserDetails")
|
||||||
|
: base(data.OrgUser, obj)
|
||||||
|
{
|
||||||
|
if (data.OrgUser == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(data.OrgUser));
|
||||||
|
}
|
||||||
|
|
||||||
|
Name = data.OrgUser.Name;
|
||||||
|
Email = data.OrgUser.Email;
|
||||||
|
AvatarColor = data.OrgUser.AvatarColor;
|
||||||
|
TwoFactorEnabled = data.TwoFactorEnabled;
|
||||||
|
SsoBound = !string.IsNullOrWhiteSpace(data.OrgUser.SsoExternalId);
|
||||||
|
Collections = data.OrgUser.Collections.Select(c => new SelectionReadOnlyResponseModel(c));
|
||||||
|
Groups = data.OrgUser.Groups;
|
||||||
|
// Prevent reset password when using key connector.
|
||||||
|
ResetPasswordEnrolled = ResetPasswordEnrolled && !data.OrgUser.UsesKeyConnector;
|
||||||
|
ClaimedByOrganization = data.ClaimedByOrganization;
|
||||||
|
}
|
||||||
|
|
||||||
public OrganizationUserUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,
|
public OrganizationUserUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,
|
||||||
bool twoFactorEnabled, bool claimedByOrganization, string obj = "organizationUserUserDetails")
|
bool twoFactorEnabled, bool claimedByOrganization, string obj = "organizationUserUserDetails")
|
||||||
: base(organizationUser, obj)
|
: base(organizationUser, obj)
|
||||||
|
@ -58,7 +58,8 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
ProviderName = organization.ProviderName;
|
ProviderName = organization.ProviderName;
|
||||||
ProviderType = organization.ProviderType;
|
ProviderType = organization.ProviderType;
|
||||||
FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName;
|
FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName;
|
||||||
FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null &&
|
IsAdminInitiated = organization.IsAdminInitiated ?? false;
|
||||||
|
FamilySponsorshipAvailable = (FamilySponsorshipFriendlyName == null || IsAdminInitiated) &&
|
||||||
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
|
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
|
||||||
.UsersCanSponsor(organization);
|
.UsersCanSponsor(organization);
|
||||||
ProductTierType = organization.PlanType.GetProductTier();
|
ProductTierType = organization.PlanType.GetProductTier();
|
||||||
@ -72,6 +73,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||||
UserIsClaimedByOrganization = organizationIdsClaimingUser.Contains(organization.OrganizationId);
|
UserIsClaimedByOrganization = organizationIdsClaimingUser.Contains(organization.OrganizationId);
|
||||||
UseRiskInsights = organization.UseRiskInsights;
|
UseRiskInsights = organization.UseRiskInsights;
|
||||||
|
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||||
|
|
||||||
if (organization.SsoConfig != null)
|
if (organization.SsoConfig != null)
|
||||||
@ -135,7 +137,6 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Obsolete.
|
/// Obsolete.
|
||||||
///
|
|
||||||
/// See <see cref="UserIsClaimedByOrganization"/>
|
/// See <see cref="UserIsClaimedByOrganization"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Please use UserIsClaimedByOrganization instead. This property will be removed in a future version.")]
|
[Obsolete("Please use UserIsClaimedByOrganization instead. This property will be removed in a future version.")]
|
||||||
@ -145,16 +146,15 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
set => UserIsClaimedByOrganization = value;
|
set => UserIsClaimedByOrganization = value;
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates if the organization claims the user.
|
/// Indicates if the user is claimed by the organization.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// An organization claims a user if the user's email domain is verified by the organization and the user is a member of it.
|
/// A user is claimed by an organization if the user's email domain is verified by the organization and the user is a member.
|
||||||
/// The organization must be enabled and able to have verified domains.
|
/// The organization must be enabled and able to have verified domains.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <returns>
|
|
||||||
/// False if the Account Deprovisioning feature flag is disabled.
|
|
||||||
/// </returns>
|
|
||||||
public bool UserIsClaimedByOrganization { get; set; }
|
public bool UserIsClaimedByOrganization { get; set; }
|
||||||
public bool UseRiskInsights { get; set; }
|
public bool UseRiskInsights { get; set; }
|
||||||
|
public bool UseOrganizationDomains { get; set; }
|
||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
|
public bool IsAdminInitiated { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
|
|||||||
LimitItemDeletion = organization.LimitItemDeletion;
|
LimitItemDeletion = organization.LimitItemDeletion;
|
||||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||||
UseRiskInsights = organization.UseRiskInsights;
|
UseRiskInsights = organization.UseRiskInsights;
|
||||||
|
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ public class MembersController : Controller
|
|||||||
{
|
{
|
||||||
return new NotFoundResult();
|
return new NotFoundResult();
|
||||||
}
|
}
|
||||||
var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser),
|
var response = new MemberResponseModel(orgUser, await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUser),
|
||||||
collections);
|
collections);
|
||||||
return new JsonResult(response);
|
return new JsonResult(response);
|
||||||
}
|
}
|
||||||
@ -185,7 +185,7 @@ public class MembersController : Controller
|
|||||||
{
|
{
|
||||||
var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
||||||
response = new MemberResponseModel(existingUserDetails,
|
response = new MemberResponseModel(existingUserDetails,
|
||||||
await _userService.TwoFactorIsEnabledAsync(existingUserDetails), associations);
|
await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(existingUserDetails), associations);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -4,8 +4,6 @@
|
|||||||
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||||
<ANCMPreConfiguredForIIS>true</ANCMPreConfiguredForIIS>
|
<ANCMPreConfiguredForIIS>true</ANCMPreConfiguredForIIS>
|
||||||
<!-- Temp exclusions until warnings are fixed -->
|
|
||||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8604</WarningsNotAsErrors>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
@ -16,6 +16,7 @@ using Bit.Core.Auth.Entities;
|
|||||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -45,6 +46,7 @@ public class AccountsController : Controller
|
|||||||
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
|
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
|
||||||
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
||||||
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
|
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
|
||||||
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
|
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
|
||||||
@ -68,6 +70,7 @@ public class AccountsController : Controller
|
|||||||
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
|
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
|
||||||
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
|
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
|
||||||
IRotateUserKeyCommand rotateUserKeyCommand,
|
IRotateUserKeyCommand rotateUserKeyCommand,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
|
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
|
||||||
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
|
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
|
||||||
@ -87,6 +90,7 @@ public class AccountsController : Controller
|
|||||||
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
|
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
|
||||||
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
|
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
|
||||||
_rotateUserKeyCommand = rotateUserKeyCommand;
|
_rotateUserKeyCommand = rotateUserKeyCommand;
|
||||||
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_cipherValidator = cipherValidator;
|
_cipherValidator = cipherValidator;
|
||||||
_folderValidator = folderValidator;
|
_folderValidator = folderValidator;
|
||||||
@ -389,7 +393,7 @@ public class AccountsController : Controller
|
|||||||
await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,
|
await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,
|
||||||
ProviderUserStatusType.Confirmed);
|
ProviderUserStatusType.Confirmed);
|
||||||
|
|
||||||
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
|
|
||||||
@ -423,7 +427,7 @@ public class AccountsController : Controller
|
|||||||
|
|
||||||
await _userService.SaveUserAsync(model.ToUser(user));
|
await _userService.SaveUserAsync(model.ToUser(user));
|
||||||
|
|
||||||
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
|
|
||||||
@ -442,7 +446,7 @@ public class AccountsController : Controller
|
|||||||
}
|
}
|
||||||
await _userService.SaveUserAsync(model.ToUser(user), true);
|
await _userService.SaveUserAsync(model.ToUser(user), true);
|
||||||
|
|
||||||
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
|
|
||||||
@ -514,9 +518,8 @@ public class AccountsController : Controller
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization.
|
// Check if the user is claimed by any organization.
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||||
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.");
|
throw new BadRequestException("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.");
|
||||||
}
|
}
|
||||||
@ -693,7 +696,6 @@ public class AccountsController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpPost("resend-new-device-otp")]
|
[HttpPost("resend-new-device-otp")]
|
||||||
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)
|
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using Bit.Api.Billing.Models.Responses;
|
using Bit.Api.Billing.Models.Responses;
|
||||||
using Bit.Core.Billing.Models.Api.Requests.Accounts;
|
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Tax.Requests;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
@ -3,6 +3,7 @@ using Bit.Api.Models.Request;
|
|||||||
using Bit.Api.Models.Request.Accounts;
|
using Bit.Api.Models.Request.Accounts;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Api.Utilities;
|
using Bit.Api.Utilities;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -22,7 +23,8 @@ namespace Bit.Api.Billing.Controllers;
|
|||||||
[Route("accounts")]
|
[Route("accounts")]
|
||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
public class AccountsController(
|
public class AccountsController(
|
||||||
IUserService userService) : Controller
|
IUserService userService,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller
|
||||||
{
|
{
|
||||||
[HttpPost("premium")]
|
[HttpPost("premium")]
|
||||||
public async Task<PaymentResponseModel> PostPremiumAsync(
|
public async Task<PaymentResponseModel> PostPremiumAsync(
|
||||||
@ -56,7 +58,7 @@ public class AccountsController(
|
|||||||
model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license,
|
model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license,
|
||||||
new TaxInfo { BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode });
|
new TaxInfo { BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode });
|
||||||
|
|
||||||
var userTwoFactorEnabled = await userService.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
|
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Billing.Models.Api.Requests.Organizations;
|
using Bit.Core.Billing.Tax.Requests;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
using System.Diagnostics;
|
||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
using Bit.Api.Billing.Models.Requests;
|
using Bit.Api.Billing.Models.Requests;
|
||||||
using Bit.Api.Billing.Models.Responses;
|
using Bit.Api.Billing.Models.Responses;
|
||||||
@ -7,7 +8,9 @@ using Bit.Core;
|
|||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Models.Sales;
|
using Bit.Core.Billing.Models.Sales;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Tax.Models;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -290,14 +293,17 @@ public class OrganizationBillingController(
|
|||||||
sale.Organization.PlanType = plan.Type;
|
sale.Organization.PlanType = plan.Type;
|
||||||
sale.Organization.Plan = plan.Name;
|
sale.Organization.Plan = plan.Name;
|
||||||
sale.SubscriptionSetup.SkipTrial = true;
|
sale.SubscriptionSetup.SkipTrial = true;
|
||||||
await organizationBillingService.Finalize(sale);
|
|
||||||
var org = await organizationRepository.GetByIdAsync(organizationId);
|
if (organizationSignup.PaymentMethodType == null || string.IsNullOrEmpty(organizationSignup.PaymentToken))
|
||||||
if (organizationSignup.PaymentMethodType != null)
|
|
||||||
{
|
{
|
||||||
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);
|
return Error.BadRequest("A payment method is required to restart the subscription.");
|
||||||
var taxInformation = TaxInformation.From(organizationSignup.TaxInfo);
|
|
||||||
await organizationBillingService.UpdatePaymentMethod(org, paymentSource, taxInformation);
|
|
||||||
}
|
}
|
||||||
|
var org = await organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
Debug.Assert(org is not null, "This organization has already been found via this same ID, this should be fine.");
|
||||||
|
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);
|
||||||
|
var taxInformation = TaxInformation.From(organizationSignup.TaxInfo);
|
||||||
|
await organizationBillingService.UpdatePaymentMethod(org, paymentSource, taxInformation);
|
||||||
|
await organizationBillingService.Finalize(sale);
|
||||||
|
|
||||||
return TypedResults.Ok();
|
return TypedResults.Ok();
|
||||||
}
|
}
|
||||||
|
@ -222,6 +222,20 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
|
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize("Application")]
|
||||||
|
[HttpDelete("{sponsoringOrgId}/{sponsoredFriendlyName}/revoke")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task AdminInitiatedRevokeSponsorshipAsync(Guid sponsoringOrgId, string sponsoredFriendlyName)
|
||||||
|
{
|
||||||
|
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);
|
||||||
|
var existingOrgSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName != null && s.FriendlyName.Equals(sponsoredFriendlyName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (existingOrgSponsorship == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("The specified sponsored organization could not be found under the given sponsoring organization.");
|
||||||
|
}
|
||||||
|
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
|
||||||
|
}
|
||||||
|
|
||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
[HttpDelete("sponsored/{sponsoredOrgId}")]
|
[HttpDelete("sponsored/{sponsoredOrgId}")]
|
||||||
[HttpPost("sponsored/{sponsoredOrgId}/remove")]
|
[HttpPost("sponsored/{sponsoredOrgId}/remove")]
|
||||||
@ -271,8 +285,11 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);
|
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);
|
||||||
return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(sponsorships.Select(s =>
|
return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(
|
||||||
new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s))));
|
sponsorships
|
||||||
|
.Where(s => s.IsAdminInitiated)
|
||||||
|
.Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,28 +109,6 @@ public class OrganizationsController(
|
|||||||
return license;
|
return license;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id:guid}/payment")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task PostPayment(Guid id, [FromBody] PaymentRequestModel model)
|
|
||||||
{
|
|
||||||
if (!await currentContext.EditPaymentMethods(id))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
await organizationService.ReplacePaymentMethodAsync(id, model.PaymentToken,
|
|
||||||
model.PaymentMethodType.Value, new TaxInfo
|
|
||||||
{
|
|
||||||
BillingAddressLine1 = model.Line1,
|
|
||||||
BillingAddressLine2 = model.Line2,
|
|
||||||
BillingAddressState = model.State,
|
|
||||||
BillingAddressCity = model.City,
|
|
||||||
BillingAddressPostalCode = model.PostalCode,
|
|
||||||
BillingAddressCountry = model.Country,
|
|
||||||
TaxIdNumber = model.TaxId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id:guid}/upgrade")]
|
[HttpPost("{id:guid}/upgrade")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<PaymentResponseModel> PostUpgrade(Guid id, [FromBody] OrganizationUpgradeRequestModel model)
|
public async Task<PaymentResponseModel> PostUpgrade(Guid id, [FromBody] OrganizationUpgradeRequestModel model)
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
using Bit.Api.Billing.Models.Requests;
|
using Bit.Api.Billing.Models.Requests;
|
||||||
using Bit.Api.Billing.Models.Responses;
|
using Bit.Api.Billing.Models.Responses;
|
||||||
|
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Models;
|
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Providers.Models;
|
||||||
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Tax.Models;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Models.BitStripe;
|
using Bit.Core.Models.BitStripe;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -147,13 +150,33 @@ public class ProviderBillingController(
|
|||||||
|
|
||||||
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
||||||
|
|
||||||
|
var getProviderPriceFromStripe = featureService.IsEnabled(FeatureFlagKeys.PM21383_GetProviderPriceFromStripe);
|
||||||
|
|
||||||
var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan =>
|
var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan =>
|
||||||
{
|
{
|
||||||
var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
|
var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
|
||||||
|
|
||||||
|
decimal unitAmount;
|
||||||
|
|
||||||
|
if (getProviderPriceFromStripe)
|
||||||
|
{
|
||||||
|
var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, plan.Type);
|
||||||
|
var price = await stripeAdapter.PriceGetAsync(priceId);
|
||||||
|
|
||||||
|
unitAmount = price.UnitAmountDecimal.HasValue
|
||||||
|
? price.UnitAmountDecimal.Value / 100M
|
||||||
|
: plan.PasswordManager.ProviderPortalSeatPrice;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
unitAmount = plan.PasswordManager.ProviderPortalSeatPrice;
|
||||||
|
}
|
||||||
|
|
||||||
return new ConfiguredProviderPlan(
|
return new ConfiguredProviderPlan(
|
||||||
providerPlan.Id,
|
providerPlan.Id,
|
||||||
providerPlan.ProviderId,
|
providerPlan.ProviderId,
|
||||||
plan,
|
plan,
|
||||||
|
unitAmount,
|
||||||
providerPlan.SeatMinimum ?? 0,
|
providerPlan.SeatMinimum ?? 0,
|
||||||
providerPlan.PurchasedSeats ?? 0,
|
providerPlan.PurchasedSeats ?? 0,
|
||||||
providerPlan.AllocatedSeats ?? 0);
|
providerPlan.AllocatedSeats ?? 0);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Tax.Services;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
|
36
src/Api/Billing/Controllers/TaxController.cs
Normal file
36
src/Api/Billing/Controllers/TaxController.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using Bit.Api.Billing.Models.Requests;
|
||||||
|
using Bit.Core.Billing.Tax.Commands;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bit.Api.Billing.Controllers;
|
||||||
|
|
||||||
|
[Authorize("Application")]
|
||||||
|
[Route("tax")]
|
||||||
|
public class TaxController(
|
||||||
|
IPreviewTaxAmountCommand previewTaxAmountCommand) : BaseBillingController
|
||||||
|
{
|
||||||
|
[HttpPost("preview-amount/organization-trial")]
|
||||||
|
public async Task<IResult> PreviewTaxAmountForOrganizationTrialAsync(
|
||||||
|
[FromBody] PreviewTaxAmountForOrganizationTrialRequestBody requestBody)
|
||||||
|
{
|
||||||
|
var parameters = new OrganizationTrialParameters
|
||||||
|
{
|
||||||
|
PlanType = requestBody.PlanType,
|
||||||
|
ProductType = requestBody.ProductType,
|
||||||
|
TaxInformation = new OrganizationTrialParameters.TaxInformationDTO
|
||||||
|
{
|
||||||
|
Country = requestBody.TaxInformation.Country,
|
||||||
|
PostalCode = requestBody.TaxInformation.PostalCode,
|
||||||
|
TaxId = requestBody.TaxInformation.TaxId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await previewTaxAmountCommand.Run(parameters);
|
||||||
|
|
||||||
|
return result.Match<IResult>(
|
||||||
|
taxAmount => TypedResults.Ok(new { TaxAmount = taxAmount }),
|
||||||
|
badRequest => Error.BadRequest(badRequest.TranslationKey),
|
||||||
|
unhandled => Error.ServerError(unhandled.TranslationKey));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Api.Billing.Models.Requests;
|
||||||
|
|
||||||
|
public class PreviewTaxAmountForOrganizationTrialRequestBody
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public PlanType PlanType { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public ProductType ProductType { get; set; }
|
||||||
|
|
||||||
|
[Required] public TaxInformationDTO TaxInformation { get; set; } = null!;
|
||||||
|
|
||||||
|
public class TaxInformationDTO
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Country { get; set; } = null!;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string PostalCode { get; set; } = null!;
|
||||||
|
|
||||||
|
public string? TaxId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Tax.Models;
|
||||||
|
|
||||||
namespace Bit.Api.Billing.Models.Requests;
|
namespace Bit.Api.Billing.Models.Requests;
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
|
using Bit.Core.Billing.Tax.Models;
|
||||||
|
|
||||||
namespace Bit.Api.Billing.Models.Responses;
|
namespace Bit.Api.Billing.Models.Responses;
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
|
using Bit.Core.Billing.Providers.Models;
|
||||||
|
using Bit.Core.Billing.Tax.Models;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Api.Billing.Models.Responses;
|
namespace Bit.Api.Billing.Models.Responses;
|
||||||
@ -34,7 +36,7 @@ public record ProviderSubscriptionResponse(
|
|||||||
.Select(providerPlan =>
|
.Select(providerPlan =>
|
||||||
{
|
{
|
||||||
var plan = providerPlan.Plan;
|
var plan = providerPlan.Plan;
|
||||||
var cost = (providerPlan.SeatMinimum + providerPlan.PurchasedSeats) * plan.PasswordManager.ProviderPortalSeatPrice;
|
var cost = (providerPlan.SeatMinimum + providerPlan.PurchasedSeats) * providerPlan.Price;
|
||||||
var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence;
|
var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence;
|
||||||
return new ProviderPlanResponse(
|
return new ProviderPlanResponse(
|
||||||
plan.Name,
|
plan.Name,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Tax.Models;
|
||||||
|
|
||||||
namespace Bit.Api.Billing.Models.Responses;
|
namespace Bit.Api.Billing.Models.Responses;
|
||||||
|
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
using Bit.Api.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Authorization.Requirements;
|
||||||
|
using Bit.Api.Models.Request.Organizations;
|
||||||
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Api.Response.OrganizationSponsorships;
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationSponsorships;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -22,6 +26,7 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
|
|||||||
private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand;
|
private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly IAuthorizationService _authorizationService;
|
||||||
|
|
||||||
public SelfHostedOrganizationSponsorshipsController(
|
public SelfHostedOrganizationSponsorshipsController(
|
||||||
ICreateSponsorshipCommand offerSponsorshipCommand,
|
ICreateSponsorshipCommand offerSponsorshipCommand,
|
||||||
@ -30,7 +35,8 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
|
|||||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IFeatureService featureService
|
IFeatureService featureService,
|
||||||
|
IAuthorizationService authorizationService
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_offerSponsorshipCommand = offerSponsorshipCommand;
|
_offerSponsorshipCommand = offerSponsorshipCommand;
|
||||||
@ -40,6 +46,7 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
|
|||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
|
_authorizationService = authorizationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{sponsoringOrgId}/families-for-enterprise")]
|
[HttpPost("{sponsoringOrgId}/families-for-enterprise")]
|
||||||
@ -84,4 +91,41 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
|
|||||||
|
|
||||||
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
|
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{sponsoringOrgId}/{sponsoredFriendlyName}/revoke")]
|
||||||
|
public async Task AdminInitiatedRevokeSponsorshipAsync(Guid sponsoringOrgId, string sponsoredFriendlyName)
|
||||||
|
{
|
||||||
|
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);
|
||||||
|
var existingOrgSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName != null && s.FriendlyName.Equals(sponsoredFriendlyName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (existingOrgSponsorship == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("The specified sponsored organization could not be found under the given sponsoring organization.");
|
||||||
|
}
|
||||||
|
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize("Application")]
|
||||||
|
[HttpGet("{orgId}/sponsored")]
|
||||||
|
public async Task<ListResponseModel<OrganizationSponsorshipInvitesResponseModel>> GetSponsoredOrganizations(Guid orgId)
|
||||||
|
{
|
||||||
|
var sponsoringOrg = await _organizationRepository.GetByIdAsync(orgId);
|
||||||
|
if (sponsoringOrg == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var authorizationResult = await _authorizationService.AuthorizeAsync(User, orgId, new ManageUsersRequirement());
|
||||||
|
if (!authorizationResult.Succeeded)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(orgId);
|
||||||
|
return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(
|
||||||
|
sponsorships
|
||||||
|
.Where(s => s.IsAdminInitiated)
|
||||||
|
.Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,18 +4,20 @@ namespace Bit.Api.Tools.Models.Response;
|
|||||||
|
|
||||||
public class MemberCipherDetailsResponseModel
|
public class MemberCipherDetailsResponseModel
|
||||||
{
|
{
|
||||||
|
public Guid? UserGuid { get; set; }
|
||||||
public string UserName { get; set; }
|
public string UserName { get; set; }
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
public bool UsesKeyConnector { get; set; }
|
public bool UsesKeyConnector { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A distinct list of the cipher ids associated with
|
/// A distinct list of the cipher ids associated with
|
||||||
/// the organization member
|
/// the organization member
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<string> CipherIds { get; set; }
|
public IEnumerable<string> CipherIds { get; set; }
|
||||||
|
|
||||||
public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails)
|
public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails)
|
||||||
{
|
{
|
||||||
|
this.UserGuid = memberAccessCipherDetails.UserGuid;
|
||||||
this.UserName = memberAccessCipherDetails.UserName;
|
this.UserName = memberAccessCipherDetails.UserName;
|
||||||
this.Email = memberAccessCipherDetails.Email;
|
this.Email = memberAccessCipherDetails.Email;
|
||||||
this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector;
|
this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector;
|
||||||
|
@ -12,17 +12,17 @@ namespace Bit.Api.KeyManagement.Validators;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class SendRotationValidator : IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>
|
public class SendRotationValidator : IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>
|
||||||
{
|
{
|
||||||
private readonly ISendService _sendService;
|
private readonly ISendAuthorizationService _sendAuthorizationService;
|
||||||
private readonly ISendRepository _sendRepository;
|
private readonly ISendRepository _sendRepository;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Instantiates a new <see cref="SendRotationValidator"/>
|
/// Instantiates a new <see cref="SendRotationValidator"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sendService">Enables conversion of <see cref="SendWithIdRequestModel"/> to <see cref="Send"/></param>
|
/// <param name="sendAuthorizationService">Enables conversion of <see cref="SendWithIdRequestModel"/> to <see cref="Send"/></param>
|
||||||
/// <param name="sendRepository">Retrieves all user <see cref="Send"/>s</param>
|
/// <param name="sendRepository">Retrieves all user <see cref="Send"/>s</param>
|
||||||
public SendRotationValidator(ISendService sendService, ISendRepository sendRepository)
|
public SendRotationValidator(ISendAuthorizationService sendAuthorizationService, ISendRepository sendRepository)
|
||||||
{
|
{
|
||||||
_sendService = sendService;
|
_sendAuthorizationService = sendAuthorizationService;
|
||||||
_sendRepository = sendRepository;
|
_sendRepository = sendRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ public class SendRotationValidator : IRotationValidator<IEnumerable<SendWithIdRe
|
|||||||
throw new BadRequestException("All existing sends must be included in the rotation.");
|
throw new BadRequestException("All existing sends must be included in the rotation.");
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Add(send.ToSend(existing, _sendService));
|
result.Add(send.ToSend(existing, _sendAuthorizationService));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -32,6 +32,7 @@ public class PlanResponseModel : ResponseModel
|
|||||||
HasTotp = plan.HasTotp;
|
HasTotp = plan.HasTotp;
|
||||||
Has2fa = plan.Has2fa;
|
Has2fa = plan.Has2fa;
|
||||||
HasSso = plan.HasSso;
|
HasSso = plan.HasSso;
|
||||||
|
HasOrganizationDomains = plan.HasOrganizationDomains;
|
||||||
HasResetPassword = plan.HasResetPassword;
|
HasResetPassword = plan.HasResetPassword;
|
||||||
UsersGetPremium = plan.UsersGetPremium;
|
UsersGetPremium = plan.UsersGetPremium;
|
||||||
UpgradeSortOrder = plan.UpgradeSortOrder;
|
UpgradeSortOrder = plan.UpgradeSortOrder;
|
||||||
@ -71,6 +72,7 @@ public class PlanResponseModel : ResponseModel
|
|||||||
public bool Has2fa { get; set; }
|
public bool Has2fa { get; set; }
|
||||||
public bool HasApi { get; set; }
|
public bool HasApi { get; set; }
|
||||||
public bool HasSso { get; set; }
|
public bool HasSso { get; set; }
|
||||||
|
public bool HasOrganizationDomains { get; set; }
|
||||||
public bool HasResetPassword { get; set; }
|
public bool HasResetPassword { get; set; }
|
||||||
public bool UsersGetPremium { get; set; }
|
public bool UsersGetPremium { get; set; }
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ using Bit.Core.Services;
|
|||||||
using Bit.Core.Tools.ImportFeatures;
|
using Bit.Core.Tools.ImportFeatures;
|
||||||
using Bit.Core.Tools.ReportFeatures;
|
using Bit.Core.Tools.ReportFeatures;
|
||||||
using Bit.Core.Auth.Models.Api.Request;
|
using Bit.Core.Auth.Models.Api.Request;
|
||||||
|
using Bit.Core.Tools.SendFeatures;
|
||||||
|
|
||||||
#if !OSS
|
#if !OSS
|
||||||
using Bit.Commercial.Core.SecretsManager;
|
using Bit.Commercial.Core.SecretsManager;
|
||||||
@ -186,6 +187,7 @@ public class Startup
|
|||||||
services.AddPhishingDomainServices(globalSettings);
|
services.AddPhishingDomainServices(globalSettings);
|
||||||
|
|
||||||
services.AddBillingQueries();
|
services.AddBillingQueries();
|
||||||
|
services.AddSendServices();
|
||||||
|
|
||||||
// Authorization Handlers
|
// Authorization Handlers
|
||||||
services.AddAuthorizationHandlers();
|
services.AddAuthorizationHandlers();
|
||||||
|
@ -12,6 +12,8 @@ using Bit.Core.Settings;
|
|||||||
using Bit.Core.Tools.Enums;
|
using Bit.Core.Tools.Enums;
|
||||||
using Bit.Core.Tools.Models.Data;
|
using Bit.Core.Tools.Models.Data;
|
||||||
using Bit.Core.Tools.Repositories;
|
using Bit.Core.Tools.Repositories;
|
||||||
|
using Bit.Core.Tools.SendFeatures;
|
||||||
|
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tools.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -25,8 +27,10 @@ public class SendsController : Controller
|
|||||||
{
|
{
|
||||||
private readonly ISendRepository _sendRepository;
|
private readonly ISendRepository _sendRepository;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly ISendService _sendService;
|
private readonly ISendAuthorizationService _sendAuthorizationService;
|
||||||
private readonly ISendFileStorageService _sendFileStorageService;
|
private readonly ISendFileStorageService _sendFileStorageService;
|
||||||
|
private readonly IAnonymousSendCommand _anonymousSendCommand;
|
||||||
|
private readonly INonAnonymousSendCommand _nonAnonymousSendCommand;
|
||||||
private readonly ILogger<SendsController> _logger;
|
private readonly ILogger<SendsController> _logger;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
@ -34,7 +38,9 @@ public class SendsController : Controller
|
|||||||
public SendsController(
|
public SendsController(
|
||||||
ISendRepository sendRepository,
|
ISendRepository sendRepository,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
ISendService sendService,
|
ISendAuthorizationService sendAuthorizationService,
|
||||||
|
IAnonymousSendCommand anonymousSendCommand,
|
||||||
|
INonAnonymousSendCommand nonAnonymousSendCommand,
|
||||||
ISendFileStorageService sendFileStorageService,
|
ISendFileStorageService sendFileStorageService,
|
||||||
ILogger<SendsController> logger,
|
ILogger<SendsController> logger,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
@ -42,13 +48,16 @@ public class SendsController : Controller
|
|||||||
{
|
{
|
||||||
_sendRepository = sendRepository;
|
_sendRepository = sendRepository;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_sendService = sendService;
|
_sendAuthorizationService = sendAuthorizationService;
|
||||||
|
_anonymousSendCommand = anonymousSendCommand;
|
||||||
|
_nonAnonymousSendCommand = nonAnonymousSendCommand;
|
||||||
_sendFileStorageService = sendFileStorageService;
|
_sendFileStorageService = sendFileStorageService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Anonymous endpoints
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpPost("access/{id}")]
|
[HttpPost("access/{id}")]
|
||||||
public async Task<IActionResult> Access(string id, [FromBody] SendAccessRequestModel model)
|
public async Task<IActionResult> Access(string id, [FromBody] SendAccessRequestModel model)
|
||||||
@ -61,18 +70,19 @@ public class SendsController : Controller
|
|||||||
//}
|
//}
|
||||||
|
|
||||||
var guid = new Guid(CoreHelpers.Base64UrlDecode(id));
|
var guid = new Guid(CoreHelpers.Base64UrlDecode(id));
|
||||||
var (send, passwordRequired, passwordInvalid) =
|
var send = await _sendRepository.GetByIdAsync(guid);
|
||||||
await _sendService.AccessAsync(guid, model.Password);
|
SendAccessResult sendAuthResult =
|
||||||
if (passwordRequired)
|
await _sendAuthorizationService.AccessAsync(send, model.Password);
|
||||||
|
if (sendAuthResult.Equals(SendAccessResult.PasswordRequired))
|
||||||
{
|
{
|
||||||
return new UnauthorizedResult();
|
return new UnauthorizedResult();
|
||||||
}
|
}
|
||||||
if (passwordInvalid)
|
if (sendAuthResult.Equals(SendAccessResult.PasswordInvalid))
|
||||||
{
|
{
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
throw new BadRequestException("Invalid password.");
|
throw new BadRequestException("Invalid password.");
|
||||||
}
|
}
|
||||||
if (send == null)
|
if (sendAuthResult.Equals(SendAccessResult.Denied))
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
@ -106,19 +116,19 @@ public class SendsController : Controller
|
|||||||
throw new BadRequestException("Could not locate send");
|
throw new BadRequestException("Could not locate send");
|
||||||
}
|
}
|
||||||
|
|
||||||
var (url, passwordRequired, passwordInvalid) = await _sendService.GetSendFileDownloadUrlAsync(send, fileId,
|
var (url, result) = await _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId,
|
||||||
model.Password);
|
model.Password);
|
||||||
|
|
||||||
if (passwordRequired)
|
if (result.Equals(SendAccessResult.PasswordRequired))
|
||||||
{
|
{
|
||||||
return new UnauthorizedResult();
|
return new UnauthorizedResult();
|
||||||
}
|
}
|
||||||
if (passwordInvalid)
|
if (result.Equals(SendAccessResult.PasswordInvalid))
|
||||||
{
|
{
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
throw new BadRequestException("Invalid password.");
|
throw new BadRequestException("Invalid password.");
|
||||||
}
|
}
|
||||||
if (send == null)
|
if (result.Equals(SendAccessResult.Denied))
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
@ -130,6 +140,45 @@ public class SendsController : Controller
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("file/validate/azure")]
|
||||||
|
public async Task<ObjectResult> AzureValidateFile()
|
||||||
|
{
|
||||||
|
return await ApiHelpers.HandleAzureEvents(Request, new Dictionary<string, Func<EventGridEvent, Task>>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"Microsoft.Storage.BlobCreated", async (eventGridEvent) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var blobName = eventGridEvent.Subject.Split($"{AzureSendFileStorageService.FilesContainerName}/blobs/")[1];
|
||||||
|
var sendId = AzureSendFileStorageService.SendIdFromBlobName(blobName);
|
||||||
|
var send = await _sendRepository.GetByIdAsync(new Guid(sendId));
|
||||||
|
if (send == null)
|
||||||
|
{
|
||||||
|
if (_sendFileStorageService is AzureSendFileStorageService azureSendFileStorageService)
|
||||||
|
{
|
||||||
|
await azureSendFileStorageService.DeleteBlobAsync(blobName);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _nonAnonymousSendCommand.ConfirmFileSize(send);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, $"Uncaught exception occurred while handling event grid event: {JsonSerializer.Serialize(eventGridEvent)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Non-anonymous endpoints
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
public async Task<SendResponseModel> Get(string id)
|
public async Task<SendResponseModel> Get(string id)
|
||||||
{
|
{
|
||||||
@ -157,8 +206,8 @@ public class SendsController : Controller
|
|||||||
{
|
{
|
||||||
model.ValidateCreation();
|
model.ValidateCreation();
|
||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
var send = model.ToSend(userId, _sendService);
|
var send = model.ToSend(userId, _sendAuthorizationService);
|
||||||
await _sendService.SaveSendAsync(send);
|
await _nonAnonymousSendCommand.SaveSendAsync(send);
|
||||||
return new SendResponseModel(send, _globalSettings);
|
return new SendResponseModel(send, _globalSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,15 +224,15 @@ public class SendsController : Controller
|
|||||||
throw new BadRequestException("Invalid content. File size hint is required.");
|
throw new BadRequestException("Invalid content. File size hint is required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.FileLength.Value > SendService.MAX_FILE_SIZE)
|
if (model.FileLength.Value > Constants.FileSize501mb)
|
||||||
{
|
{
|
||||||
throw new BadRequestException($"Max file size is {SendService.MAX_FILE_SIZE_READABLE}.");
|
throw new BadRequestException($"Max file size is {SendFileSettingHelper.MAX_FILE_SIZE_READABLE}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
model.ValidateCreation();
|
model.ValidateCreation();
|
||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
var (send, data) = model.ToSend(userId, model.File.FileName, _sendService);
|
var (send, data) = model.ToSend(userId, model.File.FileName, _sendAuthorizationService);
|
||||||
var uploadUrl = await _sendService.SaveFileSendAsync(send, data, model.FileLength.Value);
|
var uploadUrl = await _nonAnonymousSendCommand.SaveFileSendAsync(send, data, model.FileLength.Value);
|
||||||
return new SendFileUploadDataResponseModel
|
return new SendFileUploadDataResponseModel
|
||||||
{
|
{
|
||||||
Url = uploadUrl,
|
Url = uploadUrl,
|
||||||
@ -230,41 +279,7 @@ public class SendsController : Controller
|
|||||||
var send = await _sendRepository.GetByIdAsync(new Guid(id));
|
var send = await _sendRepository.GetByIdAsync(new Guid(id));
|
||||||
await Request.GetFileAsync(async (stream) =>
|
await Request.GetFileAsync(async (stream) =>
|
||||||
{
|
{
|
||||||
await _sendService.UploadFileToExistingSendAsync(stream, send);
|
await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[AllowAnonymous]
|
|
||||||
[HttpPost("file/validate/azure")]
|
|
||||||
public async Task<ObjectResult> AzureValidateFile()
|
|
||||||
{
|
|
||||||
return await ApiHelpers.HandleAzureEvents(Request, new Dictionary<string, Func<EventGridEvent, Task>>
|
|
||||||
{
|
|
||||||
{
|
|
||||||
"Microsoft.Storage.BlobCreated", async (eventGridEvent) =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var blobName = eventGridEvent.Subject.Split($"{AzureSendFileStorageService.FilesContainerName}/blobs/")[1];
|
|
||||||
var sendId = AzureSendFileStorageService.SendIdFromBlobName(blobName);
|
|
||||||
var send = await _sendRepository.GetByIdAsync(new Guid(sendId));
|
|
||||||
if (send == null)
|
|
||||||
{
|
|
||||||
if (_sendFileStorageService is AzureSendFileStorageService azureSendFileStorageService)
|
|
||||||
{
|
|
||||||
await azureSendFileStorageService.DeleteBlobAsync(blobName);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await _sendService.ValidateSendFile(send);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogError(e, $"Uncaught exception occurred while handling event grid event: {JsonSerializer.Serialize(eventGridEvent)}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,7 +294,7 @@ public class SendsController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _sendService.SaveSendAsync(model.ToSend(send, _sendService));
|
await _nonAnonymousSendCommand.SaveSendAsync(model.ToSend(send, _sendAuthorizationService));
|
||||||
return new SendResponseModel(send, _globalSettings);
|
return new SendResponseModel(send, _globalSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,7 +309,7 @@ public class SendsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
send.Password = null;
|
send.Password = null;
|
||||||
await _sendService.SaveSendAsync(send);
|
await _nonAnonymousSendCommand.SaveSendAsync(send);
|
||||||
return new SendResponseModel(send, _globalSettings);
|
return new SendResponseModel(send, _globalSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,6 +323,8 @@ public class SendsController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _sendService.DeleteSendAsync(send);
|
await _nonAnonymousSendCommand.DeleteSendAsync(send);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -36,31 +36,31 @@ public class SendRequestModel
|
|||||||
public bool? Disabled { get; set; }
|
public bool? Disabled { get; set; }
|
||||||
public bool? HideEmail { get; set; }
|
public bool? HideEmail { get; set; }
|
||||||
|
|
||||||
public Send ToSend(Guid userId, ISendService sendService)
|
public Send ToSend(Guid userId, ISendAuthorizationService sendAuthorizationService)
|
||||||
{
|
{
|
||||||
var send = new Send
|
var send = new Send
|
||||||
{
|
{
|
||||||
Type = Type,
|
Type = Type,
|
||||||
UserId = (Guid?)userId
|
UserId = (Guid?)userId
|
||||||
};
|
};
|
||||||
ToSend(send, sendService);
|
ToSend(send, sendAuthorizationService);
|
||||||
return send;
|
return send;
|
||||||
}
|
}
|
||||||
|
|
||||||
public (Send, SendFileData) ToSend(Guid userId, string fileName, ISendService sendService)
|
public (Send, SendFileData) ToSend(Guid userId, string fileName, ISendAuthorizationService sendAuthorizationService)
|
||||||
{
|
{
|
||||||
var send = ToSendBase(new Send
|
var send = ToSendBase(new Send
|
||||||
{
|
{
|
||||||
Type = Type,
|
Type = Type,
|
||||||
UserId = (Guid?)userId
|
UserId = (Guid?)userId
|
||||||
}, sendService);
|
}, sendAuthorizationService);
|
||||||
var data = new SendFileData(Name, Notes, fileName);
|
var data = new SendFileData(Name, Notes, fileName);
|
||||||
return (send, data);
|
return (send, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Send ToSend(Send existingSend, ISendService sendService)
|
public Send ToSend(Send existingSend, ISendAuthorizationService sendAuthorizationService)
|
||||||
{
|
{
|
||||||
existingSend = ToSendBase(existingSend, sendService);
|
existingSend = ToSendBase(existingSend, sendAuthorizationService);
|
||||||
switch (existingSend.Type)
|
switch (existingSend.Type)
|
||||||
{
|
{
|
||||||
case SendType.File:
|
case SendType.File:
|
||||||
@ -125,7 +125,7 @@ public class SendRequestModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Send ToSendBase(Send existingSend, ISendService sendService)
|
private Send ToSendBase(Send existingSend, ISendAuthorizationService authorizationService)
|
||||||
{
|
{
|
||||||
existingSend.Key = Key;
|
existingSend.Key = Key;
|
||||||
existingSend.ExpirationDate = ExpirationDate;
|
existingSend.ExpirationDate = ExpirationDate;
|
||||||
@ -133,7 +133,7 @@ public class SendRequestModel
|
|||||||
existingSend.MaxAccessCount = MaxAccessCount;
|
existingSend.MaxAccessCount = MaxAccessCount;
|
||||||
if (!string.IsNullOrWhiteSpace(Password))
|
if (!string.IsNullOrWhiteSpace(Password))
|
||||||
{
|
{
|
||||||
existingSend.Password = sendService.HashPassword(Password);
|
existingSend.Password = authorizationService.HashPassword(Password);
|
||||||
}
|
}
|
||||||
existingSend.Disabled = Disabled.GetValueOrDefault();
|
existingSend.Disabled = Disabled.GetValueOrDefault();
|
||||||
existingSend.HideEmail = HideEmail.GetValueOrDefault();
|
existingSend.HideEmail = HideEmail.GetValueOrDefault();
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
using Bit.Core.Models.Commands;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace Bit.Api.Utilities;
|
|
||||||
|
|
||||||
public static class CommandResultExtensions
|
|
||||||
{
|
|
||||||
public static IActionResult MapToActionResult<T>(this CommandResult<T> commandResult)
|
|
||||||
{
|
|
||||||
return commandResult switch
|
|
||||||
{
|
|
||||||
NoRecordFoundFailure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound },
|
|
||||||
BadRequestFailure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
|
|
||||||
Failure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
|
|
||||||
Success<T> success => new ObjectResult(success.Value) { StatusCode = StatusCodes.Status200OK },
|
|
||||||
_ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IActionResult MapToActionResult(this CommandResult commandResult)
|
|
||||||
{
|
|
||||||
return commandResult switch
|
|
||||||
{
|
|
||||||
NoRecordFoundFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound },
|
|
||||||
BadRequestFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
|
|
||||||
Failure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
|
|
||||||
Success => new ObjectResult(new { }) { StatusCode = StatusCodes.Status200OK },
|
|
||||||
_ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -315,26 +315,10 @@ public class CiphersController : Controller
|
|||||||
{
|
{
|
||||||
var org = _currentContext.GetOrganization(organizationId);
|
var org = _currentContext.GetOrganization(organizationId);
|
||||||
|
|
||||||
// If we're not an "admin", we don't need to check the ciphers
|
// If we're not an "admin" or if we're not a provider user we don't need to check the ciphers
|
||||||
if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true }))
|
if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true }) || await _currentContext.ProviderUserForOrgAsync(organizationId))
|
||||||
{
|
{
|
||||||
// Are we a provider user? If so, we need to be sure we're not restricted
|
return false;
|
||||||
// Once the feature flag is removed, this check can be combined with the above
|
|
||||||
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
|
|
||||||
{
|
|
||||||
// Provider is restricted from editing ciphers, so we're not an "admin"
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provider is unrestricted, so we're an "admin", don't return early
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Not a provider or admin
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We know we're an "admin", now check the ciphers explicitly (in case admins are restricted)
|
// We know we're an "admin", now check the ciphers explicitly (in case admins are restricted)
|
||||||
@ -350,26 +334,10 @@ public class CiphersController : Controller
|
|||||||
|
|
||||||
var org = _currentContext.GetOrganization(organizationId);
|
var org = _currentContext.GetOrganization(organizationId);
|
||||||
|
|
||||||
// If we're not an "admin", we don't need to check the ciphers
|
// If we're not an "admin" or if we're a provider user we don't need to check the ciphers
|
||||||
if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true }))
|
if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true }) || await _currentContext.ProviderUserForOrgAsync(organizationId))
|
||||||
{
|
{
|
||||||
// Are we a provider user? If so, we need to be sure we're not restricted
|
return false;
|
||||||
// Once the feature flag is removed, this check can be combined with the above
|
|
||||||
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
|
|
||||||
{
|
|
||||||
// Provider is restricted from editing ciphers, so we're not an "admin"
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provider is unrestricted, so we're an "admin", don't return early
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Not a provider or admin
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user can edit all ciphers for the organization, just check they all belong to the org
|
// If the user can edit all ciphers for the organization, just check they all belong to the org
|
||||||
@ -462,10 +430,10 @@ public class CiphersController : Controller
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provider users can edit all ciphers if RestrictProviderAccess is disabled
|
// Provider users cannot edit ciphers
|
||||||
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
|
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
|
||||||
{
|
{
|
||||||
return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -485,10 +453,10 @@ public class CiphersController : Controller
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provider users can only access organization ciphers if RestrictProviderAccess is disabled
|
// Provider users cannot access organization ciphers
|
||||||
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
|
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
|
||||||
{
|
{
|
||||||
return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -508,10 +476,10 @@ public class CiphersController : Controller
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provider users can only access all ciphers if RestrictProviderAccess is disabled
|
// Provider users cannot access ciphers
|
||||||
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
|
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
|
||||||
{
|
{
|
||||||
return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -1086,9 +1054,8 @@ public class CiphersController : Controller
|
|||||||
throw new BadRequestException(ModelState);
|
throw new BadRequestException(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization.
|
// Check if the user is claimed by any organization.
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||||
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details.");
|
throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details.");
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ using Bit.Core;
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -37,6 +38,7 @@ public class SyncController : Controller
|
|||||||
private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion);
|
private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion);
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
|
|
||||||
public SyncController(
|
public SyncController(
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
@ -51,7 +53,8 @@ public class SyncController : Controller
|
|||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IApplicationCacheService applicationCacheService)
|
IApplicationCacheService applicationCacheService,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_folderRepository = folderRepository;
|
_folderRepository = folderRepository;
|
||||||
@ -66,6 +69,7 @@ public class SyncController : Controller
|
|||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
@ -102,7 +106,7 @@ public class SyncController : Controller
|
|||||||
collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
|
collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
|
||||||
}
|
}
|
||||||
|
|
||||||
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(user.Id);
|
var organizationClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(user.Id);
|
||||||
var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id);
|
var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id);
|
||||||
|
@ -63,6 +63,12 @@ public class FreshdeskController : Controller
|
|||||||
note += $"<li>Region: {_billingSettings.FreshDesk.Region}</li>";
|
note += $"<li>Region: {_billingSettings.FreshDesk.Region}</li>";
|
||||||
var customFields = new Dictionary<string, object>();
|
var customFields = new Dictionary<string, object>();
|
||||||
var user = await _userRepository.GetByEmailAsync(ticketContactEmail);
|
var user = await _userRepository.GetByEmailAsync(ticketContactEmail);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
note += $"<li>No user found: {ticketContactEmail}</li>";
|
||||||
|
await CreateNote(ticketId, note);
|
||||||
|
}
|
||||||
|
|
||||||
if (user != null)
|
if (user != null)
|
||||||
{
|
{
|
||||||
var userLink = $"{_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}";
|
var userLink = $"{_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}";
|
||||||
@ -121,18 +127,7 @@ public class FreshdeskController : Controller
|
|||||||
Content = JsonContent.Create(updateBody),
|
Content = JsonContent.Create(updateBody),
|
||||||
};
|
};
|
||||||
await CallFreshdeskApiAsync(updateRequest);
|
await CallFreshdeskApiAsync(updateRequest);
|
||||||
|
await CreateNote(ticketId, note);
|
||||||
var noteBody = new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
{ "body", $"<ul>{note}</ul>" },
|
|
||||||
{ "private", true }
|
|
||||||
};
|
|
||||||
var noteRequest = new HttpRequestMessage(HttpMethod.Post,
|
|
||||||
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId))
|
|
||||||
{
|
|
||||||
Content = JsonContent.Create(noteBody),
|
|
||||||
};
|
|
||||||
await CallFreshdeskApiAsync(noteRequest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new OkResult();
|
return new OkResult();
|
||||||
@ -208,6 +203,21 @@ public class FreshdeskController : Controller
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CreateNote(string ticketId, string note)
|
||||||
|
{
|
||||||
|
var noteBody = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "body", $"<ul>{note}</ul>" },
|
||||||
|
{ "private", true }
|
||||||
|
};
|
||||||
|
var noteRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||||
|
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId))
|
||||||
|
{
|
||||||
|
Content = JsonContent.Create(noteBody),
|
||||||
|
};
|
||||||
|
await CallFreshdeskApiAsync(noteRequest);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task AddAnswerNoteToTicketAsync(string note, string ticketId)
|
private async Task AddAnswerNoteToTicketAsync(string note, string ticketId)
|
||||||
{
|
{
|
||||||
// if there is no content, then we don't need to add a note
|
// if there is no content, then we don't need to add a note
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
using Bit.Billing.Constants;
|
using Bit.Billing.Constants;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -25,8 +25,7 @@ public class UpcomingInvoiceHandler(
|
|||||||
IStripeEventService stripeEventService,
|
IStripeEventService stripeEventService,
|
||||||
IStripeEventUtilityService stripeEventUtilityService,
|
IStripeEventUtilityService stripeEventUtilityService,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IValidateSponsorshipCommand validateSponsorshipCommand,
|
IValidateSponsorshipCommand validateSponsorshipCommand)
|
||||||
IAutomaticTaxFactory automaticTaxFactory)
|
|
||||||
: IUpcomingInvoiceHandler
|
: IUpcomingInvoiceHandler
|
||||||
{
|
{
|
||||||
public async Task HandleAsync(Event parsedEvent)
|
public async Task HandleAsync(Event parsedEvent)
|
||||||
@ -46,6 +45,8 @@ public class UpcomingInvoiceHandler(
|
|||||||
|
|
||||||
var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
|
var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
|
||||||
|
|
||||||
|
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||||
|
|
||||||
if (organizationId.HasValue)
|
if (organizationId.HasValue)
|
||||||
{
|
{
|
||||||
var organization = await organizationRepository.GetByIdAsync(organizationId.Value);
|
var organization = await organizationRepository.GetByIdAsync(organizationId.Value);
|
||||||
@ -55,7 +56,7 @@ public class UpcomingInvoiceHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await TryEnableAutomaticTaxAsync(subscription);
|
await AlignOrganizationTaxConcernsAsync(organization, subscription, parsedEvent.Id, setNonUSBusinessUseToReverseCharge);
|
||||||
|
|
||||||
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||||
|
|
||||||
@ -100,7 +101,25 @@ public class UpcomingInvoiceHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await TryEnableAutomaticTaxAsync(subscription);
|
if (!subscription.AutomaticTax.Enabled && subscription.Customer.HasRecognizedTaxLocation())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await stripeFacade.UpdateSubscription(subscription.Id,
|
||||||
|
new SubscriptionUpdateOptions
|
||||||
|
{
|
||||||
|
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogError(
|
||||||
|
exception,
|
||||||
|
"Failed to set user's ({UserID}) subscription to automatic tax while processing event with ID {EventID}",
|
||||||
|
user.Id,
|
||||||
|
parsedEvent.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (user.Premium)
|
if (user.Premium)
|
||||||
{
|
{
|
||||||
@ -116,7 +135,7 @@ public class UpcomingInvoiceHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await TryEnableAutomaticTaxAsync(subscription);
|
await AlignProviderTaxConcernsAsync(provider, subscription, parsedEvent.Id, setNonUSBusinessUseToReverseCharge);
|
||||||
|
|
||||||
await SendUpcomingInvoiceEmailsAsync(new List<string> { provider.BillingEmail }, invoice);
|
await SendUpcomingInvoiceEmailsAsync(new List<string> { provider.BillingEmail }, invoice);
|
||||||
}
|
}
|
||||||
@ -139,50 +158,123 @@ public class UpcomingInvoiceHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TryEnableAutomaticTaxAsync(Subscription subscription)
|
private async Task AlignOrganizationTaxConcernsAsync(
|
||||||
|
Organization organization,
|
||||||
|
Subscription subscription,
|
||||||
|
string eventId,
|
||||||
|
bool setNonUSBusinessUseToReverseCharge)
|
||||||
{
|
{
|
||||||
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
|
var nonUSBusinessUse =
|
||||||
{
|
organization.PlanType.GetProductTier() != ProductTierType.Families &&
|
||||||
var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscription.Items.Select(x => x.Price.Id));
|
subscription.Customer.Address.Country != "US";
|
||||||
var automaticTaxStrategy = await automaticTaxFactory.CreateAsync(automaticTaxParameters);
|
|
||||||
var updateOptions = automaticTaxStrategy.GetUpdateOptions(subscription);
|
|
||||||
|
|
||||||
if (updateOptions == null)
|
bool setAutomaticTaxToEnabled;
|
||||||
|
|
||||||
|
if (setNonUSBusinessUseToReverseCharge)
|
||||||
|
{
|
||||||
|
if (nonUSBusinessUse && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
|
||||||
{
|
{
|
||||||
return;
|
try
|
||||||
|
{
|
||||||
|
await stripeFacade.UpdateCustomer(subscription.CustomerId,
|
||||||
|
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogError(
|
||||||
|
exception,
|
||||||
|
"Failed to set organization's ({OrganizationID}) to reverse tax exemption while processing event with ID {EventID}",
|
||||||
|
organization.Id,
|
||||||
|
eventId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await stripeFacade.UpdateSubscription(subscription.Id, updateOptions);
|
setAutomaticTaxToEnabled = true;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
if (subscription.AutomaticTax.Enabled ||
|
|
||||||
!subscription.Customer.HasBillingLocation() ||
|
|
||||||
await IsNonTaxableNonUSBusinessUseSubscription(subscription))
|
|
||||||
{
|
{
|
||||||
return;
|
setAutomaticTaxToEnabled =
|
||||||
|
subscription.Customer.HasRecognizedTaxLocation() &&
|
||||||
|
(subscription.Customer.Address.Country == "US" ||
|
||||||
|
(nonUSBusinessUse && subscription.Customer.TaxIds.Any()));
|
||||||
}
|
}
|
||||||
|
|
||||||
await stripeFacade.UpdateSubscription(subscription.Id,
|
if (!subscription.AutomaticTax.Enabled && setAutomaticTaxToEnabled)
|
||||||
new SubscriptionUpdateOptions
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
DefaultTaxRates = [],
|
await stripeFacade.UpdateSubscription(subscription.Id,
|
||||||
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
new SubscriptionUpdateOptions
|
||||||
});
|
{
|
||||||
|
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogError(
|
||||||
|
exception,
|
||||||
|
"Failed to set organization's ({OrganizationID}) subscription to automatic tax while processing event with ID {EventID}",
|
||||||
|
organization.Id,
|
||||||
|
eventId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
private async Task AlignProviderTaxConcernsAsync(
|
||||||
|
Provider provider,
|
||||||
|
Subscription subscription,
|
||||||
|
string eventId,
|
||||||
|
bool setNonUSBusinessUseToReverseCharge)
|
||||||
|
{
|
||||||
|
bool setAutomaticTaxToEnabled;
|
||||||
|
|
||||||
async Task<bool> IsNonTaxableNonUSBusinessUseSubscription(Subscription localSubscription)
|
if (setNonUSBusinessUseToReverseCharge)
|
||||||
{
|
{
|
||||||
var familyPriceIds = (await Task.WhenAll(
|
if (subscription.Customer.Address.Country != "US" && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
|
||||||
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019),
|
{
|
||||||
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually)))
|
try
|
||||||
.Select(plan => plan.PasswordManager.StripePlanId);
|
{
|
||||||
|
await stripeFacade.UpdateCustomer(subscription.CustomerId,
|
||||||
|
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogError(
|
||||||
|
exception,
|
||||||
|
"Failed to set provider's ({ProviderID}) to reverse tax exemption while processing event with ID {EventID}",
|
||||||
|
provider.Id,
|
||||||
|
eventId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return localSubscription.Customer.Address.Country != "US" &&
|
setAutomaticTaxToEnabled = true;
|
||||||
localSubscription.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId) &&
|
}
|
||||||
!localSubscription.Items.Select(item => item.Price.Id).Intersect(familyPriceIds).Any() &&
|
else
|
||||||
!localSubscription.Customer.TaxIds.Any();
|
{
|
||||||
|
setAutomaticTaxToEnabled =
|
||||||
|
subscription.Customer.HasRecognizedTaxLocation() &&
|
||||||
|
(subscription.Customer.Address.Country == "US" ||
|
||||||
|
subscription.Customer.TaxIds.Any());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subscription.AutomaticTax.Enabled && setAutomaticTaxToEnabled)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await stripeFacade.UpdateSubscription(subscription.Id,
|
||||||
|
new SubscriptionUpdateOptions
|
||||||
|
{
|
||||||
|
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogError(
|
||||||
|
exception,
|
||||||
|
"Failed to set provider's ({ProviderID}) subscription to automatic tax while processing event with ID {EventID}",
|
||||||
|
provider.Id,
|
||||||
|
eventId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,6 +114,11 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool UseRiskInsights { get; set; }
|
public bool UseRiskInsights { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If true, the organization can claim domains, which unlocks additional enterprise features
|
||||||
|
/// </summary>
|
||||||
|
public bool UseOrganizationDomains { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If set to true, admins can initiate organization-issued sponsorships.
|
/// If set to true, admins can initiate organization-issued sponsorships.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -319,5 +324,7 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
|||||||
SmSeats = license.SmSeats;
|
SmSeats = license.SmSeats;
|
||||||
SmServiceAccounts = license.SmServiceAccounts;
|
SmServiceAccounts = license.SmServiceAccounts;
|
||||||
UseRiskInsights = license.UseRiskInsights;
|
UseRiskInsights = license.UseRiskInsights;
|
||||||
|
UseOrganizationDomains = license.UseOrganizationDomains;
|
||||||
|
UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ public class OrganizationAbility
|
|||||||
LimitItemDeletion = organization.LimitItemDeletion;
|
LimitItemDeletion = organization.LimitItemDeletion;
|
||||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||||
UseRiskInsights = organization.UseRiskInsights;
|
UseRiskInsights = organization.UseRiskInsights;
|
||||||
|
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,5 +47,6 @@ public class OrganizationAbility
|
|||||||
public bool LimitItemDeletion { get; set; }
|
public bool LimitItemDeletion { get; set; }
|
||||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||||
public bool UseRiskInsights { get; set; }
|
public bool UseRiskInsights { get; set; }
|
||||||
|
public bool UseOrganizationDomains { get; set; }
|
||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -59,5 +59,7 @@ public class OrganizationUserOrganizationDetails
|
|||||||
public bool LimitItemDeletion { get; set; }
|
public bool LimitItemDeletion { get; set; }
|
||||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||||
public bool UseRiskInsights { get; set; }
|
public bool UseRiskInsights { get; set; }
|
||||||
|
public bool UseOrganizationDomains { get; set; }
|
||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
|
public bool? IsAdminInitiated { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -150,6 +150,7 @@ public class SelfHostedOrganizationDetails : Organization
|
|||||||
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
|
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
|
||||||
Status = Status,
|
Status = Status,
|
||||||
UseRiskInsights = UseRiskInsights,
|
UseRiskInsights = UseRiskInsights,
|
||||||
|
UseAdminSponsoredFamilies = UseAdminSponsoredFamilies,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,7 @@ public class ProviderUserOrganizationDetails
|
|||||||
public bool LimitItemDeletion { get; set; }
|
public bool LimitItemDeletion { get; set; }
|
||||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||||
public bool UseRiskInsights { get; set; }
|
public bool UseRiskInsights { get; set; }
|
||||||
|
public bool UseOrganizationDomains { get; set; }
|
||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
public ProviderType ProviderType { get; set; }
|
public ProviderType ProviderType { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@ public class VerifyOrganizationDomainCommand(
|
|||||||
IDnsResolverService dnsResolverService,
|
IDnsResolverService dnsResolverService,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
IFeatureService featureService,
|
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
ISavePolicyCommand savePolicyCommand,
|
ISavePolicyCommand savePolicyCommand,
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
@ -125,11 +124,8 @@ public class VerifyOrganizationDomainCommand(
|
|||||||
|
|
||||||
private async Task DomainVerificationSideEffectsAsync(OrganizationDomain domain, IActingUser actingUser)
|
private async Task DomainVerificationSideEffectsAsync(OrganizationDomain domain, IActingUser actingUser)
|
||||||
{
|
{
|
||||||
if (featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser);
|
||||||
{
|
await SendVerifiedDomainUserEmailAsync(domain);
|
||||||
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser);
|
|
||||||
await SendVerifiedDomainUserEmailAsync(domain);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser) =>
|
private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser) =>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -24,6 +25,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
|||||||
private readonly IPolicyService _policyService;
|
private readonly IPolicyService _policyService;
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||||
|
|
||||||
public AcceptOrgUserCommand(
|
public AcceptOrgUserCommand(
|
||||||
@ -34,6 +36,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
|||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
|
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
|
||||||
{
|
{
|
||||||
|
|
||||||
@ -45,6 +48,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
|||||||
_policyService = policyService;
|
_policyService = policyService;
|
||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,7 +196,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enforce Two Factor Authentication Policy of organization user is trying to join
|
// Enforce Two Factor Authentication Policy of organization user is trying to join
|
||||||
if (!await userService.TwoFactorIsEnabledAsync(user))
|
if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
|
||||||
{
|
{
|
||||||
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
|
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
|
||||||
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
|
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
|
||||||
|
@ -24,9 +24,7 @@ public class GetOrganizationUsersClaimedStatusQuery : IGetOrganizationUsersClaim
|
|||||||
// Users can only be claimed by an Organization that is enabled and can have organization domains
|
// Users can only be claimed by an Organization that is enabled and can have organization domains
|
||||||
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
|
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
|
||||||
|
|
||||||
// TODO: Replace "UseSso" with a new organization ability like "UseOrganizationDomains" (PM-11622).
|
if (organizationAbility is { Enabled: true, UseOrganizationDomains: true })
|
||||||
// Verified domains were tied to SSO, so we currently check the "UseSso" organization ability.
|
|
||||||
if (organizationAbility is { Enabled: true, UseSso: true })
|
|
||||||
{
|
{
|
||||||
// Get all organization users with claimed domains by the organization
|
// Get all organization users with claimed domains by the organization
|
||||||
var organizationUsersWithClaimedDomain = await _organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organizationId);
|
var organizationUsersWithClaimedDomain = await _organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organizationId);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
|
||||||
|
@ -6,4 +6,8 @@ namespace Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
|||||||
public interface IOrganizationUserUserDetailsQuery
|
public interface IOrganizationUserUserDetailsQuery
|
||||||
{
|
{
|
||||||
Task<IEnumerable<OrganizationUserUserDetails>> GetOrganizationUserUserDetails(OrganizationUserUserDetailsQueryRequest request);
|
Task<IEnumerable<OrganizationUserUserDetails>> GetOrganizationUserUserDetails(OrganizationUserUserDetailsQueryRequest request);
|
||||||
|
|
||||||
|
Task<IEnumerable<(OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization)>> Get(OrganizationUserUserDetailsQueryRequest request);
|
||||||
|
|
||||||
|
Task<IEnumerable<(OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization)>> GetAccountRecoveryEnrolledUsers(OrganizationUserUserDetailsQueryRequest request);
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user