mirror of
https://github.com/bitwarden/server.git
synced 2025-05-22 12:04:27 -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.yml @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)
|
||||
**/packages.lock.json
|
||||
|
66
.github/renovate.json5
vendored
66
.github/renovate.json5
vendored
@ -20,7 +20,7 @@
|
||||
],
|
||||
commitMessagePrefix: "[deps] BRE:",
|
||||
reviewers: ["team:dept-bre"],
|
||||
addLabels: ["hold"]
|
||||
addLabels: ["hold"],
|
||||
},
|
||||
{
|
||||
groupName: "dockerfile minor",
|
||||
@ -37,6 +37,16 @@
|
||||
matchManagers: ["github-actions"],
|
||||
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"],
|
||||
commitMessagePrefix: "[deps] BRE:",
|
||||
@ -59,6 +69,7 @@
|
||||
"DuoUniversal",
|
||||
"Fido2.AspNet",
|
||||
"Duende.IdentityServer",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer",
|
||||
"Microsoft.Extensions.Identity.Stores",
|
||||
"Otp.NET",
|
||||
"Sustainsys.Saml2.AspNetCore2",
|
||||
@ -79,8 +90,6 @@
|
||||
"CsvHelper",
|
||||
"Kralizek.AutoFixture.Extensions.MockHttp",
|
||||
"Microsoft.AspNetCore.Mvc.Testing",
|
||||
"Microsoft.Extensions.Logging",
|
||||
"Microsoft.Extensions.Logging.Console",
|
||||
"Newtonsoft.Json",
|
||||
"NSubstitute",
|
||||
"Sentry.Serilog",
|
||||
@ -100,9 +109,9 @@
|
||||
reviewers: ["team:team-billing-dev"],
|
||||
},
|
||||
{
|
||||
matchPackagePatterns: ["^Microsoft.Extensions.Logging"],
|
||||
groupName: "Microsoft.Extensions.Logging",
|
||||
description: "Group Microsoft.Extensions.Logging to exclude them from the dotnet monorepo preset",
|
||||
matchPackageNames: ["/^Microsoft\\.EntityFrameworkCore\\./", "/^dotnet-ef/"],
|
||||
groupName: "EntityFrameworkCore",
|
||||
description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset",
|
||||
},
|
||||
{
|
||||
matchPackageNames: [
|
||||
@ -117,9 +126,6 @@
|
||||
"Microsoft.EntityFrameworkCore.Relational",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer",
|
||||
"Microsoft.Extensions.Caching.Cosmos",
|
||||
"Microsoft.Extensions.Caching.SqlServer",
|
||||
"Microsoft.Extensions.Caching.StackExchangeRedis",
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL",
|
||||
"Pomelo.EntityFrameworkCore.MySql",
|
||||
],
|
||||
@ -142,56 +148,40 @@
|
||||
"Azure.Messaging.ServiceBus",
|
||||
"Azure.Storage.Blobs",
|
||||
"Azure.Storage.Queues",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer",
|
||||
"LaunchDarkly.ServerSdk",
|
||||
"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",
|
||||
],
|
||||
description: "Platform owned dependencies",
|
||||
commitMessagePrefix: "[deps] Platform:",
|
||||
reviewers: ["team:team-platform-dev"],
|
||||
},
|
||||
{
|
||||
matchPackagePatterns: ["EntityFrameworkCore", "^dotnet-ef"],
|
||||
groupName: "EntityFrameworkCore",
|
||||
description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset",
|
||||
},
|
||||
{
|
||||
matchPackageNames: [
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection",
|
||||
"AWSSDK.SimpleEmail",
|
||||
"AWSSDK.SQS",
|
||||
"Handlebars.Net",
|
||||
"LaunchDarkly.ServerSdk",
|
||||
"MailKit",
|
||||
"Microsoft.AspNetCore.SignalR.Protocols.MessagePack",
|
||||
"Microsoft.AspNetCore.SignalR.StackExchangeRedis",
|
||||
"Microsoft.Azure.NotificationHubs",
|
||||
"Microsoft.Extensions.Configuration.EnvironmentVariables",
|
||||
"Microsoft.Extensions.Configuration.UserSecrets",
|
||||
"Microsoft.Extensions.Configuration",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions",
|
||||
"Microsoft.Extensions.DependencyInjection",
|
||||
"SendGrid",
|
||||
],
|
||||
description: "Tools owned dependencies",
|
||||
commitMessagePrefix: "[deps] Tools:",
|
||||
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: [
|
||||
"AngleSharp",
|
||||
|
302
.github/workflows/build.yml
vendored
302
.github/workflows/build.yml
vendored
@ -14,11 +14,12 @@ on:
|
||||
|
||||
env:
|
||||
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
||||
_GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@ -31,12 +32,11 @@ jobs:
|
||||
- name: Verify format
|
||||
run: dotnet format --verify-no-changes
|
||||
|
||||
build-container:
|
||||
name: Build container images
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
id-token: write
|
||||
security-events: write
|
||||
build-artifacts:
|
||||
name: Build artifacts
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- lint
|
||||
outputs:
|
||||
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
|
||||
strategy:
|
||||
@ -45,10 +45,9 @@ jobs:
|
||||
include:
|
||||
- project_name: Admin
|
||||
base_path: ./src
|
||||
node: true
|
||||
- project_name: Api
|
||||
base_path: ./src
|
||||
- project_name: Attachments
|
||||
base_path: ./util
|
||||
- project_name: Billing
|
||||
base_path: ./src
|
||||
- project_name: Events
|
||||
@ -59,20 +58,21 @@ jobs:
|
||||
base_path: ./src
|
||||
- project_name: Identity
|
||||
base_path: ./src
|
||||
- project_name: MsSql
|
||||
base_path: ./util
|
||||
- project_name: MsSqlMigratorUtility
|
||||
base_path: ./util
|
||||
- project_name: Nginx
|
||||
base_path: ./util
|
||||
dotnet: true
|
||||
- project_name: Notifications
|
||||
base_path: ./src
|
||||
- project_name: Scim
|
||||
base_path: ./bitwarden_license/src
|
||||
dotnet: true
|
||||
- project_name: Server
|
||||
base_path: ./util
|
||||
- project_name: Setup
|
||||
base_path: ./util
|
||||
- project_name: Sso
|
||||
base_path: ./bitwarden_license/src
|
||||
node: true
|
||||
steps:
|
||||
- name: Check secrets
|
||||
id: check-secrets
|
||||
@ -87,6 +87,116 @@ jobs:
|
||||
with:
|
||||
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
|
||||
env:
|
||||
PUBLISH_BRANCHES: "main,rc,hotfix-rc"
|
||||
@ -100,13 +210,6 @@ jobs:
|
||||
echo "is_publish_branch=false" >> $GITHUB_ENV
|
||||
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 ##########
|
||||
- name: Log in to Azure - production subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
@ -116,22 +219,40 @@ jobs:
|
||||
- name: Log in to ACR - production subscription
|
||||
run: az acr login -n bitwardenprod
|
||||
|
||||
########## Generate image tag and build container image ##########
|
||||
- name: Generate container image tag
|
||||
- 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"
|
||||
|
||||
########## Generate image tag and build Docker image ##########
|
||||
- name: Generate Docker image tag
|
||||
id: tag
|
||||
run: |
|
||||
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
|
||||
IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s#/#-#g")
|
||||
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" || "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then
|
||||
IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize branch name to alphanumeric only
|
||||
else
|
||||
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
||||
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
|
||||
IMAGE_TAG=dev
|
||||
fi
|
||||
|
||||
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
|
||||
id: setup
|
||||
@ -156,26 +277,30 @@ jobs:
|
||||
fi
|
||||
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate image full name
|
||||
id: cache-name
|
||||
env:
|
||||
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||
run: echo "name=${_AZ_REGISTRY}/${PROJECT_NAME}:buildcache" >> $GITHUB_OUTPUT
|
||||
- name: Get build artifact
|
||||
if: ${{ matrix.dotnet }}
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
with:
|
||||
name: ${{ matrix.project_name }}.zip
|
||||
|
||||
- name: Build Container image
|
||||
id: build-container
|
||||
- name: Set up build artifact
|
||||
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
|
||||
with:
|
||||
cache-from: type=registry,ref=${{ steps.cache-name.outputs.name }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-name.outputs.name}},mode=max
|
||||
context: .
|
||||
context: ${{ matrix.base_path }}/${{ matrix.project_name }}
|
||||
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
|
||||
platforms: |
|
||||
linux/amd64,
|
||||
linux/arm/v7,
|
||||
linux/arm64
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.image-tags.outputs.tags }}
|
||||
secrets: |
|
||||
"GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}"
|
||||
|
||||
- name: Install Cosign
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
@ -184,7 +309,7 @@ jobs:
|
||||
- name: Sign image with Cosign
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
env:
|
||||
DIGEST: ${{ steps.build-container.outputs.digest }}
|
||||
DIGEST: ${{ steps.build-docker.outputs.digest }}
|
||||
TAGS: ${{ steps.image-tags.outputs.tags }}
|
||||
run: |
|
||||
IFS="," read -a tags <<< "${TAGS}"
|
||||
@ -194,7 +319,7 @@ jobs:
|
||||
done
|
||||
cosign sign --yes ${images}
|
||||
|
||||
- name: Scan container image
|
||||
- name: Scan Docker image
|
||||
id: container-scan
|
||||
uses: anchore/scan-action@abae793926ec39a78ab18002bc7fc45bbbd94342 # v6.0.0
|
||||
with:
|
||||
@ -209,10 +334,10 @@ jobs:
|
||||
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 }}
|
||||
|
||||
build-stub-swagger:
|
||||
name: Build Docker-Stub/Swagger
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-container
|
||||
upload:
|
||||
name: Upload
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build-docker
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@ -227,11 +352,8 @@ jobs:
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
|
||||
- name: Login to PROD ACR
|
||||
run: az acr login -n ${_AZ_REGISTRY%.azurecr.io}
|
||||
|
||||
- name: Restore
|
||||
run: dotnet tool restore
|
||||
- name: Log in to ACR - production subscription
|
||||
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
||||
|
||||
- name: Make Docker stubs
|
||||
if: |
|
||||
@ -326,10 +448,8 @@ jobs:
|
||||
- name: Build Public API Swagger
|
||||
run: |
|
||||
cd ./src/Api
|
||||
echo "Restore"
|
||||
dotnet restore --locked-mode
|
||||
echo "Clean"
|
||||
dotnet clean -c "Release" -o obj/build-output/publish
|
||||
echo "Restore tools"
|
||||
dotnet tool restore
|
||||
echo "Publish"
|
||||
dotnet publish -c "Release" -o obj/build-output/publish
|
||||
|
||||
@ -392,7 +512,9 @@ jobs:
|
||||
|
||||
build-mssqlmigratorutility:
|
||||
name: Build MSSQL migrator utility
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- lint
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@ -420,11 +542,6 @@ jobs:
|
||||
echo "GitHub ref: $GITHUB_REF"
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
|
||||
- name: Restore project
|
||||
run: |
|
||||
echo "Restore"
|
||||
dotnet restore --locked-mode
|
||||
|
||||
- name: Publish project
|
||||
run: |
|
||||
dotnet publish -c "Release" -o obj/build-output/publish -r ${{ matrix.target }} -p:PublishSingleFile=true \
|
||||
@ -451,8 +568,9 @@ jobs:
|
||||
if: |
|
||||
github.event_name != 'pull_request'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-container
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- build-docker
|
||||
steps:
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
@ -484,8 +602,9 @@ jobs:
|
||||
trigger-k8s-deploy:
|
||||
name: Trigger k8s deploy
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-container
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- build-docker
|
||||
steps:
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
@ -515,49 +634,13 @@ jobs:
|
||||
}
|
||||
})
|
||||
|
||||
trigger-ee-updates:
|
||||
name: Trigger Ephemeral Environment updates
|
||||
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
|
||||
setup-ephemeral-environment:
|
||||
name: Setup Ephemeral Environment
|
||||
needs:
|
||||
- build-container
|
||||
- trigger-ee-updates
|
||||
- build-artifacts
|
||||
- build-docker
|
||||
if: |
|
||||
needs.build-container.outputs.has_secrets == 'true'
|
||||
needs.build-artifacts.outputs.has_secrets == 'true'
|
||||
&& github.event_name == 'pull_request'
|
||||
&& contains(github.event.pull_request.labels.*.name, 'ephemeral-environment')
|
||||
uses: bitwarden/gh-actions/.github/workflows/_ephemeral_environment_manager.yml@main
|
||||
@ -569,16 +652,15 @@ jobs:
|
||||
check-failures:
|
||||
name: Check for failures
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- lint
|
||||
- build-container
|
||||
- build-stub-swagger
|
||||
- build-artifacts
|
||||
- build-docker
|
||||
- upload
|
||||
- build-mssqlmigratorutility
|
||||
- self-host-build
|
||||
- trigger-k8s-deploy
|
||||
- trigger-ee-updates
|
||||
- trigger-ephemeral-environment-sync
|
||||
steps:
|
||||
- name: Check if any job failed
|
||||
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:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
types: [opened, synchronize, reopened]
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
|
8
.github/workflows/scan.yml
vendored
8
.github/workflows/scan.yml
vendored
@ -7,8 +7,14 @@ on:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc"
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches-ignore:
|
||||
- main
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
types: [opened, synchronize, reopened]
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
jobs:
|
||||
check-run:
|
||||
|
@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2025.5.0</Version>
|
||||
<Version>2025.5.1</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
@ -129,6 +129,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "t
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.IntegrationTest", "test\Core.IntegrationTest\Core.IntegrationTest.csproj", "{3631BA42-6731-4118-A917-DAA43C5032B9}"
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -377,6 +389,8 @@ Global
|
||||
{4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {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}
|
||||
{9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||
{17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
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.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Entities;
|
||||
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.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
|
@ -7,13 +7,12 @@ using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Commercial.Core.AdminConsole.Providers;
|
||||
@ -23,7 +22,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IStripeAdapter _stripeAdapter;
|
||||
private readonly IFeatureService _featureService;
|
||||
@ -31,26 +29,22 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
private readonly ISubscriberService _subscriberService;
|
||||
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IAutomaticTaxStrategy _automaticTaxStrategy;
|
||||
|
||||
public RemoveOrganizationFromProviderCommand(
|
||||
IEventService eventService,
|
||||
IMailService mailService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationService organizationService,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IStripeAdapter stripeAdapter,
|
||||
IFeatureService featureService,
|
||||
IProviderBillingService providerBillingService,
|
||||
ISubscriberService subscriberService,
|
||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
||||
IPricingClient pricingClient,
|
||||
[FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy)
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_eventService = eventService;
|
||||
_mailService = mailService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationService = organizationService;
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_stripeAdapter = stripeAdapter;
|
||||
_featureService = featureService;
|
||||
@ -58,7 +52,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
_subscriberService = subscriberService;
|
||||
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
||||
_pricingClient = pricingClient;
|
||||
_automaticTaxStrategy = automaticTaxStrategy;
|
||||
}
|
||||
|
||||
public async Task RemoveOrganizationFromProvider(
|
||||
@ -76,7 +69,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
|
||||
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(
|
||||
providerOrganization.OrganizationId,
|
||||
Array.Empty<Guid>(),
|
||||
[],
|
||||
includeProvider: false))
|
||||
{
|
||||
throw new BadRequestException("Organization must have at least one confirmed owner.");
|
||||
@ -101,7 +94,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private async Task ResetOrganizationBillingAsync(
|
||||
@ -141,15 +134,18 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
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(
|
||||
organization.Id,
|
||||
organization.Name,
|
||||
provider.Name,
|
||||
provider.Name!,
|
||||
organizationOwnerEmails);
|
||||
}
|
||||
}
|
||||
|
@ -5,12 +5,13 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -53,6 +54,7 @@ public class ProviderService : IProviderService
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IProviderClientOrganizationSignUpCommand _providerClientOrganizationSignUpCommand;
|
||||
|
||||
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
|
||||
@ -61,7 +63,8 @@ public class ProviderService : IProviderService
|
||||
IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
|
||||
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,
|
||||
IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,
|
||||
IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient)
|
||||
IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient,
|
||||
IProviderClientOrganizationSignUpCommand providerClientOrganizationSignUpCommand)
|
||||
{
|
||||
_providerRepository = providerRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
@ -81,6 +84,7 @@ public class ProviderService : IProviderService
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_providerBillingService = providerBillingService;
|
||||
_pricingClient = pricingClient;
|
||||
_providerClientOrganizationSignUpCommand = providerClientOrganizationSignUpCommand;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
var (organization, _, defaultCollection) = await _organizationService.SignupClientAsync(organizationSignup);
|
||||
var signUpResponse = await _providerClientOrganizationSignUpCommand.SignUpClientOrganizationAsync(organizationSignup);
|
||||
|
||||
var providerOrganization = new ProviderOrganization
|
||||
{
|
||||
ProviderId = providerId,
|
||||
OrganizationId = organization.Id,
|
||||
OrganizationId = signUpResponse.Organization.Id,
|
||||
Key = organizationSignup.OwnerKey,
|
||||
};
|
||||
|
||||
@ -574,12 +578,12 @@ public class ProviderService : IProviderService
|
||||
|
||||
// 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
|
||||
var defaultOwnerAccess = defaultCollection != null
|
||||
var defaultOwnerAccess = signUpResponse.DefaultCollection != null
|
||||
?
|
||||
[
|
||||
new CollectionAccessSelection
|
||||
{
|
||||
Id = defaultCollection.Id,
|
||||
Id = signUpResponse.DefaultCollection.Id,
|
||||
HidePasswords = false,
|
||||
ReadOnly = false,
|
||||
Manage = true
|
||||
@ -587,7 +591,7 @@ public class ProviderService : IProviderService
|
||||
]
|
||||
: 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)[]
|
||||
{
|
||||
(
|
||||
|
@ -1,8 +1,8 @@
|
||||
using System.Globalization;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Providers.Entities;
|
||||
using CsvHelper.Configuration.Attributes;
|
||||
|
||||
namespace Bit.Commercial.Core.Billing.Models;
|
||||
namespace Bit.Commercial.Core.Billing.Providers.Models;
|
||||
|
||||
public class ProviderClientInvoiceReportRow
|
||||
{
|
@ -7,11 +7,12 @@ using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
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.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -24,7 +25,7 @@ using Microsoft.Extensions.Logging;
|
||||
using OneOf;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Commercial.Core.Billing;
|
||||
namespace Bit.Commercial.Core.Billing.Providers.Services;
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion)]
|
||||
public class BusinessUnitConverter(
|
||||
@ -67,6 +68,7 @@ public class BusinessUnitConverter(
|
||||
organization.MaxStorageGb = updatedPlan.PasswordManager.BaseStorageGb;
|
||||
organization.UsePolicies = updatedPlan.HasPolicies;
|
||||
organization.UseSso = updatedPlan.HasSso;
|
||||
organization.UseOrganizationDomains = updatedPlan.HasOrganizationDomains;
|
||||
organization.UseGroups = updatedPlan.HasGroups;
|
||||
organization.UseEvents = updatedPlan.HasEvents;
|
||||
organization.UseDirectory = updatedPlan.HasDirectory;
|
@ -1,5 +1,5 @@
|
||||
using System.Globalization;
|
||||
using Bit.Commercial.Core.Billing.Models;
|
||||
using Bit.Commercial.Core.Billing.Providers.Models;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
@ -8,15 +8,17 @@ using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
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.Contracts;
|
||||
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
@ -25,15 +27,13 @@ using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Braintree;
|
||||
using CsvHelper;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Stripe;
|
||||
|
||||
using static Bit.Core.Billing.Utilities;
|
||||
using Customer = Stripe.Customer;
|
||||
using Subscription = Stripe.Subscription;
|
||||
|
||||
namespace Bit.Commercial.Core.Billing;
|
||||
namespace Bit.Commercial.Core.Billing.Providers.Services;
|
||||
|
||||
public class ProviderBillingService(
|
||||
IBraintreeGateway braintreeGateway,
|
||||
@ -50,8 +50,7 @@ public class ProviderBillingService(
|
||||
ISetupIntentCache setupIntentCache,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ISubscriberService subscriberService,
|
||||
ITaxService taxService,
|
||||
[FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy)
|
||||
ITaxService taxService)
|
||||
: IProviderBillingService
|
||||
{
|
||||
public async Task AddExistingOrganization(
|
||||
@ -97,6 +96,7 @@ public class ProviderBillingService(
|
||||
organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb;
|
||||
organization.UsePolicies = plan.HasPolicies;
|
||||
organization.UseSso = plan.HasSso;
|
||||
organization.UseOrganizationDomains = plan.HasOrganizationDomains;
|
||||
organization.UseGroups = plan.HasGroups;
|
||||
organization.UseEvents = plan.HasEvents;
|
||||
organization.UseDirectory = plan.HasDirectory;
|
||||
@ -125,7 +125,7 @@ public class ProviderBillingService(
|
||||
|
||||
/*
|
||||
* 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);
|
||||
|
||||
@ -233,7 +233,7 @@ public class ProviderBillingService(
|
||||
|
||||
var providerCustomer = await subscriberService.GetCustomerOrThrow(provider, new CustomerGetOptions
|
||||
{
|
||||
Expand = ["tax_ids"]
|
||||
Expand = ["tax", "tax_ids"]
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
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))
|
||||
{
|
||||
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}'.",
|
||||
taxInfo.BillingAddressCountry,
|
||||
taxInfo.TaxIdNumber);
|
||||
|
||||
throw new BadRequestException("billingTaxIdTypeInferenceError");
|
||||
}
|
||||
|
||||
@ -692,6 +707,13 @@ public class ProviderBillingService(
|
||||
customer.Metadata.ContainsKey(BraintreeCustomerIdKey) ||
|
||||
setupIntent.IsUnverifiedBankAccount());
|
||||
|
||||
int? trialPeriodDays = provider.Type switch
|
||||
{
|
||||
ProviderType.Msp when usePaymentMethod => 14,
|
||||
ProviderType.BusinessUnit when usePaymentMethod => 4,
|
||||
_ => null
|
||||
};
|
||||
|
||||
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
||||
{
|
||||
CollectionMethod = usePaymentMethod ?
|
||||
@ -705,17 +727,24 @@ public class ProviderBillingService(
|
||||
},
|
||||
OffSession = true,
|
||||
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
|
||||
TrialPeriodDays = usePaymentMethod ? 14 : null
|
||||
TrialPeriodDays = trialPeriodDays
|
||||
};
|
||||
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
|
||||
{
|
||||
automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer);
|
||||
}
|
||||
else
|
||||
var setNonUSBusinessUseToReverseCharge =
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||
|
||||
if (setNonUSBusinessUseToReverseCharge)
|
||||
{
|
||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
else if (customer.HasRecognizedTaxLocation())
|
||||
{
|
||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||
{
|
||||
Enabled = customer.Address.Country == "US" ||
|
||||
customer.TaxIds.Any()
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
@ -6,7 +6,7 @@ using Bit.Core.Billing;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Commercial.Core.Billing;
|
||||
namespace Bit.Commercial.Core.Billing.Providers.Services;
|
||||
|
||||
public static class ProviderPriceAdapter
|
||||
{
|
@ -1,13 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Licenses;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
||||
@ -17,72 +13,42 @@ public class MaxProjectsQuery : IMaxProjectsQuery
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly ILicensingService _licensingService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public MaxProjectsQuery(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IProjectRepository projectRepository,
|
||||
IGlobalSettings globalSettings,
|
||||
ILicensingService licensingService,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_projectRepository = projectRepository;
|
||||
_globalSettings = globalSettings;
|
||||
_licensingService = licensingService;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
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);
|
||||
if (org == null)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
var projects = await _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId);
|
||||
return ((short? max, bool? overMax))(projects + projectsToAdd > maxProjects ? (maxProjects, true) : (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.");
|
||||
return ((short? max, bool? overMax))(projects + projectsToAdd > plan.SecretsManager.MaxProjects ? (plan.SecretsManager.MaxProjects, true) : (plan.SecretsManager.MaxProjects, false));
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
using Bit.Commercial.Core.AdminConsole.Providers;
|
||||
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.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
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.Errors;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Commands;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -76,9 +76,8 @@ public class PostUserCommand(
|
||||
var invitedOrganizationUserId = result switch
|
||||
{
|
||||
Success<ScimInviteOrganizationUsersResponse> success => success.Value.InvitedUser.Id,
|
||||
Failure<ScimInviteOrganizationUsersResponse> failure when failure.Errors
|
||||
.Any(x => x.Message == NoUsersToInviteError.Code) => (Guid?)null,
|
||||
Failure<ScimInviteOrganizationUsersResponse> failure when failure.Errors.Length != 0 => throw MapToBitException(failure.Errors),
|
||||
Failure<ScimInviteOrganizationUsersResponse> { Error.Message: NoUsersToInviteError.Code } => (Guid?)null,
|
||||
Failure<ScimInviteOrganizationUsersResponse> failure => throw MapToBitException(failure.Error),
|
||||
_ => 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": {
|
||||
"css-loader": "7.1.2",
|
||||
"expose-loader": "5.0.0",
|
||||
"expose-loader": "5.0.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"sass": "1.85.0",
|
||||
"sass-loader": "16.0.4",
|
||||
@ -1083,9 +1083,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/expose-loader": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.0.tgz",
|
||||
"integrity": "sha512-BtUqYRmvx1bEY5HN6eK2I9URUZgNmN0x5UANuocaNjXSgfoDlkXt+wyEMe7i5DzDNh2BKJHPc5F4rBwEdSQX6w==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.1.tgz",
|
||||
"integrity": "sha512-5YPZuszN/eWND/B+xuq5nIpb/l5TV1HYmdO6SubYtHv+HenVw9/6bn33Mm5reY8DNid7AVtbARvyUD34edfCtg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
@ -14,7 +14,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"css-loader": "7.1.2",
|
||||
"expose-loader": "5.0.0",
|
||||
"expose-loader": "5.0.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"sass": "1.85.0",
|
||||
"sass-loader": "16.0.4",
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Commercial.Core.AdminConsole.Providers;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
@ -7,6 +8,7 @@ using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -223,31 +225,115 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
|
||||
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
|
||||
{
|
||||
Id = "subscription_id"
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IAutomaticTaxStrategy>()
|
||||
.When(x => x.SetCreateOptions(
|
||||
Arg.Is<SubscriptionCreateOptions>(options =>
|
||||
options.Customer == organization.GatewayCustomerId &&
|
||||
options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
|
||||
options.DaysUntilDue == 30 &&
|
||||
options.Metadata["organizationId"] == organization.Id.ToString() &&
|
||||
options.OffSession == true &&
|
||||
options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&
|
||||
options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId &&
|
||||
options.Items.First().Quantity == organization.Seats)
|
||||
, Arg.Any<Customer>()))
|
||||
.Do(x =>
|
||||
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||
|
||||
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
||||
options.Customer == organization.GatewayCustomerId &&
|
||||
options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
|
||||
options.DaysUntilDue == 30 &&
|
||||
options.AutomaticTax.Enabled == true &&
|
||||
options.Metadata["organizationId"] == organization.Id.ToString() &&
|
||||
options.OffSession == true &&
|
||||
options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&
|
||||
options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId &&
|
||||
options.Items.First().Quantity == organization.Seats));
|
||||
|
||||
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 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.Models.Business.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -717,8 +718,8 @@ public class ProviderServiceTests
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
||||
.Returns((organization, null as OrganizationUser, new Collection()));
|
||||
sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
|
||||
.Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));
|
||||
|
||||
var providerOrganization =
|
||||
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
|
||||
@ -755,8 +756,8 @@ public class ProviderServiceTests
|
||||
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
||||
.Returns((organization, null as OrganizationUser, new Collection()));
|
||||
sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
|
||||
.Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user));
|
||||
@ -782,8 +783,8 @@ public class ProviderServiceTests
|
||||
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
||||
.Returns((organization, null as OrganizationUser, new Collection()));
|
||||
sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
|
||||
.Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));
|
||||
|
||||
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);
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
||||
.Returns((organization, null as OrganizationUser, defaultCollection));
|
||||
sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
|
||||
.Returns(new ProviderClientOrganizationSignUpResponse(organization, defaultCollection));
|
||||
|
||||
var providerOrganization =
|
||||
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
|
||||
|
@ -1,16 +1,16 @@
|
||||
#nullable enable
|
||||
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.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
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.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -25,7 +25,7 @@ using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.Billing;
|
||||
namespace Bit.Commercial.Core.Test.Billing.Providers;
|
||||
|
||||
public class BusinessUnitConverterTests
|
||||
{
|
@ -1,7 +1,7 @@
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using Bit.Commercial.Core.Billing;
|
||||
using Bit.Commercial.Core.Billing.Models;
|
||||
using Bit.Commercial.Core.Billing.Providers.Models;
|
||||
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
@ -10,13 +10,14 @@ using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
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.Contracts;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -39,7 +40,7 @@ using Customer = Stripe.Customer;
|
||||
using PaymentMethod = Stripe.PaymentMethod;
|
||||
using Subscription = Stripe.Subscription;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.Billing;
|
||||
namespace Bit.Commercial.Core.Test.Billing.Providers;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class ProviderBillingServiceTests
|
||||
@ -261,7 +262,7 @@ public class ProviderBillingServiceTests
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri
|
||||
@ -311,6 +312,91 @@ public class ProviderBillingServiceTests
|
||||
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
|
||||
|
||||
#region GenerateClientInvoiceReport
|
||||
@ -1181,6 +1267,62 @@ public class ProviderBillingServiceTests
|
||||
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]
|
||||
public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
@ -1306,7 +1448,7 @@ public class ProviderBillingServiceTests
|
||||
.Returns(new Customer
|
||||
{
|
||||
Id = "customer_id",
|
||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||
Address = new Address { Country = "US" }
|
||||
});
|
||||
|
||||
var providerPlans = new List<ProviderPlan>
|
||||
@ -1358,7 +1500,7 @@ public class ProviderBillingServiceTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "customer_id",
|
||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetCustomerOrThrow(
|
||||
@ -1398,19 +1540,6 @@ public class ProviderBillingServiceTests
|
||||
|
||||
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>(
|
||||
sub =>
|
||||
sub.AutomaticTax.Enabled == true &&
|
||||
@ -1442,11 +1571,11 @@ public class ProviderBillingServiceTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "customer_id",
|
||||
Address = new Address { Country = "US" },
|
||||
InvoiceSettings = new CustomerInvoiceSettings
|
||||
{
|
||||
DefaultPaymentMethodId = "pm_123"
|
||||
},
|
||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
@ -1487,19 +1616,6 @@ public class ProviderBillingServiceTests
|
||||
|
||||
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>()
|
||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||
|
||||
@ -1535,9 +1651,9 @@ public class ProviderBillingServiceTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "customer_id",
|
||||
Address = new Address { Country = "US" },
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>(),
|
||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
@ -1578,19 +1694,6 @@ public class ProviderBillingServiceTests
|
||||
|
||||
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>()
|
||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||
|
||||
@ -1645,12 +1748,15 @@ public class ProviderBillingServiceTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "customer_id",
|
||||
Address = new Address
|
||||
{
|
||||
Country = "US"
|
||||
},
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["btCustomerId"] = "braintree_customer_id"
|
||||
},
|
||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
@ -1691,22 +1797,92 @@ public class ProviderBillingServiceTests
|
||||
|
||||
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 =>
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||
sub =>
|
||||
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
|
||||
{
|
||||
Enabled = true
|
||||
};
|
||||
});
|
||||
DefaultPaymentMethodId = "pm_123"
|
||||
}
|
||||
};
|
||||
|
||||
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>()
|
||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||
sub =>
|
||||
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.Enums.Provider;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.Billing;
|
||||
namespace Bit.Commercial.Core.Test.Billing.Providers;
|
||||
|
||||
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.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.Billing;
|
||||
namespace Bit.Commercial.Core.Test.Billing.Tax;
|
||||
|
||||
[SutProviderCustomize]
|
||||
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.Billing.Enums;
|
||||
using Bit.Core.Billing.Licenses;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
@ -22,11 +18,26 @@ namespace Bit.Commercial.Core.Test.SecretsManager.Queries.Projects;
|
||||
[SutProviderCustomize]
|
||||
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]
|
||||
[BitAutoData]
|
||||
public async Task GetByOrgIdAsync_OrganizationIsNull_ThrowsNotFound(SutProvider<MaxProjectsQuery> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(default).ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1));
|
||||
@ -35,54 +46,6 @@ public class MaxProjectsQueryTests
|
||||
.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]
|
||||
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||
[BitAutoData(PlanType.TeamsMonthly2020)]
|
||||
@ -97,57 +60,16 @@ public class MaxProjectsQueryTests
|
||||
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually2020)]
|
||||
[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)
|
||||
{
|
||||
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);
|
||||
|
||||
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;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
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);
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
|
||||
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, 4, 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)
|
||||
{
|
||||
organization.PlanType = planType;
|
||||
@ -191,66 +113,8 @@ public class MaxProjectsQueryTests
|
||||
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(projects);
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||
var plan = StaticStore.GetPlan(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);
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
|
||||
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);
|
||||
|
||||
|
@ -11,6 +11,7 @@ MAILCATCHER_PORT=1080
|
||||
# Alternative databases
|
||||
POSTGRES_PASSWORD=SET_A_PASSWORD_HERE_123
|
||||
MYSQL_ROOT_PASSWORD=SET_A_PASSWORD_HERE_123
|
||||
MARIADB_ROOT_PASSWORD=SET_A_PASSWORD_HERE_123
|
||||
|
||||
# IdP configuration
|
||||
# Complete using the values from the Manage SSO page in the web vault
|
||||
|
@ -70,6 +70,20 @@ services:
|
||||
profiles:
|
||||
- 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:
|
||||
image: kenchan0130/simplesamlphp:1.19.8
|
||||
container_name: idp
|
||||
|
@ -5,6 +5,7 @@ param(
|
||||
[switch]$all,
|
||||
[switch]$postgres,
|
||||
[switch]$mysql,
|
||||
[switch]$mariadb,
|
||||
[switch]$mssql,
|
||||
[switch]$sqlite,
|
||||
[switch]$selfhost,
|
||||
@ -15,11 +16,15 @@ param(
|
||||
$ErrorActionPreference = "Stop"
|
||||
$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;
|
||||
}
|
||||
|
||||
if ($all -or $postgres -or $mysql -or $sqlite) {
|
||||
if ($all -or $(Get-IsEFDatabase)) {
|
||||
dotnet ef *> $null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
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 @(
|
||||
@($mysql, "MySQL", "MySqlMigrations", "mySql", 2),
|
||||
@($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) {
|
||||
continue
|
||||
|
@ -4,6 +4,7 @@
|
||||
"rollForward": "latestFeature"
|
||||
},
|
||||
"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["username"] = username;
|
||||
payload["password"] = password;
|
||||
|
||||
params.headers["Auth-Email"] = encoding.b64encode(username);
|
||||
} else {
|
||||
payload["scope"] = "api.organization";
|
||||
payload["grant_type"] = "client_credentials";
|
||||
|
@ -11,7 +11,7 @@ using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.OrganizationConnectionConfigs;
|
||||
@ -462,6 +462,7 @@ public class OrganizationsController : Controller
|
||||
organization.UsersGetPremium = model.UsersGetPremium;
|
||||
organization.UseSecretsManager = model.UseSecretsManager;
|
||||
organization.UseRiskInsights = model.UseRiskInsights;
|
||||
organization.UseOrganizationDomains = model.UseOrganizationDomains;
|
||||
organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies;
|
||||
|
||||
//secrets
|
||||
|
@ -10,13 +10,13 @@ using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
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.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
|
@ -102,7 +102,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats;
|
||||
SmServiceAccounts = org.SmServiceAccounts;
|
||||
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
|
||||
|
||||
UseOrganizationDomains = org.UseOrganizationDomains;
|
||||
_plans = plans;
|
||||
}
|
||||
|
||||
@ -186,6 +186,8 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
public int? SmServiceAccounts { get; set; }
|
||||
[Display(Name = "Max Autoscale Machine Accounts")]
|
||||
public int? MaxAutoscaleSmServiceAccounts { get; set; }
|
||||
[Display(Name = "Use Organization Domains")]
|
||||
public bool UseOrganizationDomains { get; set; }
|
||||
|
||||
/**
|
||||
* Creates a Plan[] object for use in Javascript
|
||||
@ -215,6 +217,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
Has2fa = p.Has2fa,
|
||||
HasApi = p.HasApi,
|
||||
HasSso = p.HasSso,
|
||||
HasOrganizationDomains = p.HasOrganizationDomains,
|
||||
HasKeyConnector = p.HasKeyConnector,
|
||||
HasScim = p.HasScim,
|
||||
HasResetPassword = p.HasResetPassword,
|
||||
@ -315,6 +318,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
existingOrganization.MaxAutoscaleSmSeats = MaxAutoscaleSmSeats;
|
||||
existingOrganization.SmServiceAccounts = SmServiceAccounts;
|
||||
existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts;
|
||||
existingOrganization.UseOrganizationDomains = UseOrganizationDomains;
|
||||
return existingOrganization;
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,8 @@
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Providers.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
|
||||
|
@ -2,8 +2,8 @@
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Providers.Entities;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
|
@ -124,6 +124,10 @@
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseSso" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseSso"></label>
|
||||
</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">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseKeyConnector" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<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.UsePolicies))').checked = plan.hasPolicies;
|
||||
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.UseDirectory))').checked = plan.hasDirectory;
|
||||
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.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
|
@ -1,8 +1,8 @@
|
||||
using Bit.Admin.Billing.Models;
|
||||
using Bit.Admin.Enums;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core.Billing.Migration.Models;
|
||||
using Bit.Core.Billing.Migration.Services;
|
||||
using Bit.Core.Billing.Providers.Migration.Models;
|
||||
using Bit.Core.Billing.Providers.Migration.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Providers.Entities;
|
||||
|
||||
namespace Bit.Admin.Billing.Models;
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
@using System.Text.Json
|
||||
@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult
|
||||
@model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult
|
||||
@{
|
||||
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";
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ using Bit.Admin.Enums;
|
||||
using Bit.Admin.Models;
|
||||
using Bit.Admin.Services;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -89,7 +88,7 @@ public class UsersController : Controller
|
||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@ -106,7 +105,7 @@ public class UsersController : Controller
|
||||
var billingInfo = await _paymentService.GetBillingAsync(user);
|
||||
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(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);
|
||||
|
||||
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired));
|
||||
@ -167,7 +166,6 @@ public class UsersController : Controller
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.User_NewDeviceException_Edit)]
|
||||
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
|
||||
public async Task<IActionResult> ToggleNewDeviceVerification(Guid id)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(id);
|
||||
@ -179,12 +177,4 @@ public class UsersController : Controller
|
||||
await _userService.ToggleNewDeviceVerificationException(user.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.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))
|
||||
{
|
||||
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 Bit.Admin.Services;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Migration;
|
||||
using Bit.Core.Billing.Providers.Migration;
|
||||
|
||||
#if !OSS
|
||||
using Bit.Commercial.Core.Utilities;
|
||||
|
@ -2,7 +2,7 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Jobs;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||
using Quartz;
|
||||
|
||||
namespace Bit.Admin.Tools.Jobs;
|
||||
@ -32,10 +32,10 @@ public class DeleteSendsJob : BaseJob
|
||||
}
|
||||
using (var scope = _serviceProvider.CreateScope())
|
||||
{
|
||||
var sendService = scope.ServiceProvider.GetRequiredService<ISendService>();
|
||||
var nonAnonymousSendCommand = scope.ServiceProvider.GetRequiredService<INonAnonymousSendCommand>();
|
||||
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": {
|
||||
"css-loader": "7.1.2",
|
||||
"expose-loader": "5.0.0",
|
||||
"expose-loader": "5.0.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"sass": "1.85.0",
|
||||
"sass-loader": "16.0.4",
|
||||
@ -1084,9 +1084,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/expose-loader": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.0.tgz",
|
||||
"integrity": "sha512-BtUqYRmvx1bEY5HN6eK2I9URUZgNmN0x5UANuocaNjXSgfoDlkXt+wyEMe7i5DzDNh2BKJHPc5F4rBwEdSQX6w==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.1.tgz",
|
||||
"integrity": "sha512-5YPZuszN/eWND/B+xuq5nIpb/l5TV1HYmdO6SubYtHv+HenVw9/6bn33Mm5reY8DNid7AVtbARvyUD34edfCtg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
@ -15,7 +15,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"css-loader": "7.1.2",
|
||||
"expose-loader": "5.0.0",
|
||||
"expose-loader": "5.0.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"sass": "1.85.0",
|
||||
"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.Response.Organizations;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -137,7 +135,6 @@ public class OrganizationDomainController : Controller
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("domain/sso/verified")]
|
||||
[RequireFeature(FeatureFlagKeys.VerifiedSsoDomainEndpoint)]
|
||||
public async Task<VerifiedOrganizationDomainSsoDetailsResponseModel> GetVerifiedOrgDomainSsoDetailsAsync(
|
||||
[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.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Response;
|
||||
@ -162,6 +163,12 @@ public class OrganizationUsersController : Controller
|
||||
[HttpGet("")]
|
||||
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(
|
||||
User, new OrganizationScope(orgId), OrganizationUserUserDetailsOperations.ReadAll)).Succeeded;
|
||||
if (!authorized)
|
||||
@ -191,6 +198,37 @@ public class OrganizationUsersController : Controller
|
||||
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")]
|
||||
public async Task<IEnumerable<string>> GetGroups(string orgId, string id)
|
||||
{
|
||||
@ -578,7 +616,6 @@ public class OrganizationUsersController : Controller
|
||||
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
|
||||
}
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
||||
[HttpDelete("{id}/delete-account")]
|
||||
[HttpPost("{id}/delete-account")]
|
||||
public async Task DeleteAccount(Guid orgId, Guid id)
|
||||
@ -597,7 +634,6 @@ public class OrganizationUsersController : Controller
|
||||
await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
|
||||
}
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
||||
[HttpDelete("delete-account")]
|
||||
[HttpPost("delete-account")]
|
||||
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)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
return userIds.ToDictionary(kvp => kvp, kvp => false);
|
||||
}
|
||||
|
||||
var usersOrganizationClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgId, userIds);
|
||||
return usersOrganizationClaimedStatus;
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
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.");
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& (await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id))
|
||||
if ((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.");
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
using Bit.Api.AdminConsole.Models.Response.Helpers;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
@ -79,7 +78,7 @@ public class PoliciesController : Controller
|
||||
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);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
|
@ -75,6 +75,8 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
||||
|
||||
public string InitiationPath { get; set; }
|
||||
|
||||
public bool SkipTrial { get; set; }
|
||||
|
||||
public virtual OrganizationSignup ToOrganizationSignup(User user)
|
||||
{
|
||||
var orgSignup = new OrganizationSignup
|
||||
@ -107,6 +109,7 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
||||
BillingAddressCountry = BillingAddressCountry,
|
||||
},
|
||||
InitiationPath = InitiationPath,
|
||||
SkipTrial = SkipTrial
|
||||
};
|
||||
|
||||
Keys?.ToOrganizationSignup(orgSignup);
|
||||
|
@ -64,6 +64,7 @@ public class OrganizationResponseModel : ResponseModel
|
||||
LimitItemDeletion = organization.LimitItemDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
UseRiskInsights = organization.UseRiskInsights;
|
||||
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||
}
|
||||
|
||||
@ -111,6 +112,7 @@ public class OrganizationResponseModel : ResponseModel
|
||||
public bool LimitItemDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseOrganizationDomains { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
}
|
||||
|
||||
|
@ -126,6 +126,26 @@ public class OrganizationUserUserMiniDetailsResponseModel : ResponseModel
|
||||
|
||||
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,
|
||||
bool twoFactorEnabled, bool claimedByOrganization, string obj = "organizationUserUserDetails")
|
||||
: base(organizationUser, obj)
|
||||
|
@ -58,7 +58,8 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
ProviderName = organization.ProviderName;
|
||||
ProviderType = organization.ProviderType;
|
||||
FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName;
|
||||
FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null &&
|
||||
IsAdminInitiated = organization.IsAdminInitiated ?? false;
|
||||
FamilySponsorshipAvailable = (FamilySponsorshipFriendlyName == null || IsAdminInitiated) &&
|
||||
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
|
||||
.UsersCanSponsor(organization);
|
||||
ProductTierType = organization.PlanType.GetProductTier();
|
||||
@ -72,6 +73,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
UserIsClaimedByOrganization = organizationIdsClaimingUser.Contains(organization.OrganizationId);
|
||||
UseRiskInsights = organization.UseRiskInsights;
|
||||
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||
|
||||
if (organization.SsoConfig != null)
|
||||
@ -135,7 +137,6 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
/// <summary>
|
||||
/// Obsolete.
|
||||
///
|
||||
/// See <see cref="UserIsClaimedByOrganization"/>
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
/// <summary>
|
||||
/// Indicates if the organization claims the user.
|
||||
/// Indicates if the user is claimed by the organization.
|
||||
/// </summary>
|
||||
/// <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.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// False if the Account Deprovisioning feature flag is disabled.
|
||||
/// </returns>
|
||||
public bool UserIsClaimedByOrganization { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseOrganizationDomains { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public bool IsAdminInitiated { get; set; }
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
|
||||
LimitItemDeletion = organization.LimitItemDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
UseRiskInsights = organization.UseRiskInsights;
|
||||
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||
}
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ public class MembersController : Controller
|
||||
{
|
||||
return new NotFoundResult();
|
||||
}
|
||||
var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser),
|
||||
var response = new MemberResponseModel(orgUser, await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUser),
|
||||
collections);
|
||||
return new JsonResult(response);
|
||||
}
|
||||
@ -185,7 +185,7 @@ public class MembersController : Controller
|
||||
{
|
||||
var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
||||
response = new MemberResponseModel(existingUserDetails,
|
||||
await _userService.TwoFactorIsEnabledAsync(existingUserDetails), associations);
|
||||
await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(existingUserDetails), associations);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -4,8 +4,6 @@
|
||||
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||
<ANCMPreConfiguredForIIS>true</ANCMPreConfiguredForIIS>
|
||||
<!-- Temp exclusions until warnings are fixed -->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8604</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<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.Data;
|
||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -45,6 +46,7 @@ public class AccountsController : Controller
|
||||
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
|
||||
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
||||
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
|
||||
@ -68,6 +70,7 @@ public class AccountsController : Controller
|
||||
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
|
||||
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
|
||||
IRotateUserKeyCommand rotateUserKeyCommand,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IFeatureService featureService,
|
||||
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
|
||||
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
|
||||
@ -87,6 +90,7 @@ public class AccountsController : Controller
|
||||
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
|
||||
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
|
||||
_rotateUserKeyCommand = rotateUserKeyCommand;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_featureService = featureService;
|
||||
_cipherValidator = cipherValidator;
|
||||
_folderValidator = folderValidator;
|
||||
@ -389,7 +393,7 @@ public class AccountsController : Controller
|
||||
await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,
|
||||
ProviderUserStatusType.Confirmed);
|
||||
|
||||
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
||||
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
|
||||
@ -423,7 +427,7 @@ public class AccountsController : Controller
|
||||
|
||||
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 organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
|
||||
@ -442,7 +446,7 @@ public class AccountsController : Controller
|
||||
}
|
||||
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 organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
|
||||
@ -514,9 +518,8 @@ public class AccountsController : Controller
|
||||
}
|
||||
else
|
||||
{
|
||||
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization.
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||
// Check if the user is claimed by any organization.
|
||||
if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||
{
|
||||
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]
|
||||
[HttpPost("resend-new-device-otp")]
|
||||
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#nullable enable
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Core.Billing.Models.Api.Requests.Accounts;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -3,6 +3,7 @@ using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Request.Accounts;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
@ -22,7 +23,8 @@ namespace Bit.Api.Billing.Controllers;
|
||||
[Route("accounts")]
|
||||
[Authorize("Application")]
|
||||
public class AccountsController(
|
||||
IUserService userService) : Controller
|
||||
IUserService userService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller
|
||||
{
|
||||
[HttpPost("premium")]
|
||||
public async Task<PaymentResponseModel> PostPremiumAsync(
|
||||
@ -56,7 +58,7 @@ public class AccountsController(
|
||||
model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license,
|
||||
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 organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
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.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
@ -1,4 +1,5 @@
|
||||
#nullable enable
|
||||
using System.Diagnostics;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
@ -7,7 +8,9 @@ using Bit.Core;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -290,14 +293,17 @@ public class OrganizationBillingController(
|
||||
sale.Organization.PlanType = plan.Type;
|
||||
sale.Organization.Plan = plan.Name;
|
||||
sale.SubscriptionSetup.SkipTrial = true;
|
||||
await organizationBillingService.Finalize(sale);
|
||||
var org = await organizationRepository.GetByIdAsync(organizationId);
|
||||
if (organizationSignup.PaymentMethodType != null)
|
||||
|
||||
if (organizationSignup.PaymentMethodType == null || string.IsNullOrEmpty(organizationSignup.PaymentToken))
|
||||
{
|
||||
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);
|
||||
var taxInformation = TaxInformation.From(organizationSignup.TaxInfo);
|
||||
await organizationBillingService.UpdatePaymentMethod(org, paymentSource, taxInformation);
|
||||
return Error.BadRequest("A payment method is required to restart the subscription.");
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
@ -222,6 +222,20 @@ public class OrganizationSponsorshipsController : Controller
|
||||
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")]
|
||||
[HttpDelete("sponsored/{sponsoredOrgId}")]
|
||||
[HttpPost("sponsored/{sponsoredOrgId}/remove")]
|
||||
@ -271,8 +285,11 @@ public class OrganizationSponsorshipsController : Controller
|
||||
}
|
||||
|
||||
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);
|
||||
return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(sponsorships.Select(s =>
|
||||
new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s))));
|
||||
return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(
|
||||
sponsorships
|
||||
.Where(s => s.IsAdminInitiated)
|
||||
.Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
@ -109,28 +109,6 @@ public class OrganizationsController(
|
||||
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")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
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.Responses;
|
||||
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Models;
|
||||
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.Tax.Models;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Models.BitStripe;
|
||||
using Bit.Core.Services;
|
||||
@ -147,13 +150,33 @@ public class ProviderBillingController(
|
||||
|
||||
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
||||
|
||||
var getProviderPriceFromStripe = featureService.IsEnabled(FeatureFlagKeys.PM21383_GetProviderPriceFromStripe);
|
||||
|
||||
var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan =>
|
||||
{
|
||||
var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
|
||||
|
||||
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(
|
||||
providerPlan.Id,
|
||||
providerPlan.ProviderId,
|
||||
plan,
|
||||
unitAmount,
|
||||
providerPlan.SeatMinimum ?? 0,
|
||||
providerPlan.PurchasedSeats ?? 0,
|
||||
providerPlan.AllocatedSeats ?? 0);
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
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 Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
|
@ -2,6 +2,8 @@
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Providers.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
@ -34,7 +36,7 @@ public record ProviderSubscriptionResponse(
|
||||
.Select(providerPlan =>
|
||||
{
|
||||
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;
|
||||
return new ProviderPlanResponse(
|
||||
plan.Name,
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
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.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.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -22,6 +26,7 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
|
||||
private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
public SelfHostedOrganizationSponsorshipsController(
|
||||
ICreateSponsorshipCommand offerSponsorshipCommand,
|
||||
@ -30,7 +35,8 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
|
||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService
|
||||
IFeatureService featureService,
|
||||
IAuthorizationService authorizationService
|
||||
)
|
||||
{
|
||||
_offerSponsorshipCommand = offerSponsorshipCommand;
|
||||
@ -40,6 +46,7 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_currentContext = currentContext;
|
||||
_featureService = featureService;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
[HttpPost("{sponsoringOrgId}/families-for-enterprise")]
|
||||
@ -84,4 +91,41 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
|
||||
|
||||
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 Guid? UserGuid { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string Email { get; set; }
|
||||
public bool UsesKeyConnector { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A distinct list of the cipher ids associated with
|
||||
/// A distinct list of the cipher ids associated with
|
||||
/// the organization member
|
||||
/// </summary>
|
||||
public IEnumerable<string> CipherIds { get; set; }
|
||||
|
||||
public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails)
|
||||
{
|
||||
this.UserGuid = memberAccessCipherDetails.UserGuid;
|
||||
this.UserName = memberAccessCipherDetails.UserName;
|
||||
this.Email = memberAccessCipherDetails.Email;
|
||||
this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector;
|
||||
|
@ -12,17 +12,17 @@ namespace Bit.Api.KeyManagement.Validators;
|
||||
/// </summary>
|
||||
public class SendRotationValidator : IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>
|
||||
{
|
||||
private readonly ISendService _sendService;
|
||||
private readonly ISendAuthorizationService _sendAuthorizationService;
|
||||
private readonly ISendRepository _sendRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a new <see cref="SendRotationValidator"/>
|
||||
/// </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>
|
||||
public SendRotationValidator(ISendService sendService, ISendRepository sendRepository)
|
||||
public SendRotationValidator(ISendAuthorizationService sendAuthorizationService, ISendRepository sendRepository)
|
||||
{
|
||||
_sendService = sendService;
|
||||
_sendAuthorizationService = sendAuthorizationService;
|
||||
_sendRepository = sendRepository;
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ public class SendRotationValidator : IRotationValidator<IEnumerable<SendWithIdRe
|
||||
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;
|
||||
|
@ -32,6 +32,7 @@ public class PlanResponseModel : ResponseModel
|
||||
HasTotp = plan.HasTotp;
|
||||
Has2fa = plan.Has2fa;
|
||||
HasSso = plan.HasSso;
|
||||
HasOrganizationDomains = plan.HasOrganizationDomains;
|
||||
HasResetPassword = plan.HasResetPassword;
|
||||
UsersGetPremium = plan.UsersGetPremium;
|
||||
UpgradeSortOrder = plan.UpgradeSortOrder;
|
||||
@ -71,6 +72,7 @@ public class PlanResponseModel : ResponseModel
|
||||
public bool Has2fa { get; set; }
|
||||
public bool HasApi { get; set; }
|
||||
public bool HasSso { get; set; }
|
||||
public bool HasOrganizationDomains { get; set; }
|
||||
public bool HasResetPassword { get; set; }
|
||||
public bool UsersGetPremium { get; set; }
|
||||
|
||||
|
@ -35,6 +35,7 @@ using Bit.Core.Services;
|
||||
using Bit.Core.Tools.ImportFeatures;
|
||||
using Bit.Core.Tools.ReportFeatures;
|
||||
using Bit.Core.Auth.Models.Api.Request;
|
||||
using Bit.Core.Tools.SendFeatures;
|
||||
|
||||
#if !OSS
|
||||
using Bit.Commercial.Core.SecretsManager;
|
||||
@ -186,6 +187,7 @@ public class Startup
|
||||
services.AddPhishingDomainServices(globalSettings);
|
||||
|
||||
services.AddBillingQueries();
|
||||
services.AddSendServices();
|
||||
|
||||
// Authorization Handlers
|
||||
services.AddAuthorizationHandlers();
|
||||
|
@ -12,6 +12,8 @@ using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
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.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -25,8 +27,10 @@ public class SendsController : Controller
|
||||
{
|
||||
private readonly ISendRepository _sendRepository;
|
||||
private readonly IUserService _userService;
|
||||
private readonly ISendService _sendService;
|
||||
private readonly ISendAuthorizationService _sendAuthorizationService;
|
||||
private readonly ISendFileStorageService _sendFileStorageService;
|
||||
private readonly IAnonymousSendCommand _anonymousSendCommand;
|
||||
private readonly INonAnonymousSendCommand _nonAnonymousSendCommand;
|
||||
private readonly ILogger<SendsController> _logger;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
@ -34,7 +38,9 @@ public class SendsController : Controller
|
||||
public SendsController(
|
||||
ISendRepository sendRepository,
|
||||
IUserService userService,
|
||||
ISendService sendService,
|
||||
ISendAuthorizationService sendAuthorizationService,
|
||||
IAnonymousSendCommand anonymousSendCommand,
|
||||
INonAnonymousSendCommand nonAnonymousSendCommand,
|
||||
ISendFileStorageService sendFileStorageService,
|
||||
ILogger<SendsController> logger,
|
||||
GlobalSettings globalSettings,
|
||||
@ -42,13 +48,16 @@ public class SendsController : Controller
|
||||
{
|
||||
_sendRepository = sendRepository;
|
||||
_userService = userService;
|
||||
_sendService = sendService;
|
||||
_sendAuthorizationService = sendAuthorizationService;
|
||||
_anonymousSendCommand = anonymousSendCommand;
|
||||
_nonAnonymousSendCommand = nonAnonymousSendCommand;
|
||||
_sendFileStorageService = sendFileStorageService;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
_currentContext = currentContext;
|
||||
}
|
||||
|
||||
#region Anonymous endpoints
|
||||
[AllowAnonymous]
|
||||
[HttpPost("access/{id}")]
|
||||
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 (send, passwordRequired, passwordInvalid) =
|
||||
await _sendService.AccessAsync(guid, model.Password);
|
||||
if (passwordRequired)
|
||||
var send = await _sendRepository.GetByIdAsync(guid);
|
||||
SendAccessResult sendAuthResult =
|
||||
await _sendAuthorizationService.AccessAsync(send, model.Password);
|
||||
if (sendAuthResult.Equals(SendAccessResult.PasswordRequired))
|
||||
{
|
||||
return new UnauthorizedResult();
|
||||
}
|
||||
if (passwordInvalid)
|
||||
if (sendAuthResult.Equals(SendAccessResult.PasswordInvalid))
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
throw new BadRequestException("Invalid password.");
|
||||
}
|
||||
if (send == null)
|
||||
if (sendAuthResult.Equals(SendAccessResult.Denied))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -106,19 +116,19 @@ public class SendsController : Controller
|
||||
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);
|
||||
|
||||
if (passwordRequired)
|
||||
if (result.Equals(SendAccessResult.PasswordRequired))
|
||||
{
|
||||
return new UnauthorizedResult();
|
||||
}
|
||||
if (passwordInvalid)
|
||||
if (result.Equals(SendAccessResult.PasswordInvalid))
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
throw new BadRequestException("Invalid password.");
|
||||
}
|
||||
if (send == null)
|
||||
if (result.Equals(SendAccessResult.Denied))
|
||||
{
|
||||
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}")]
|
||||
public async Task<SendResponseModel> Get(string id)
|
||||
{
|
||||
@ -157,8 +206,8 @@ public class SendsController : Controller
|
||||
{
|
||||
model.ValidateCreation();
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var send = model.ToSend(userId, _sendService);
|
||||
await _sendService.SaveSendAsync(send);
|
||||
var send = model.ToSend(userId, _sendAuthorizationService);
|
||||
await _nonAnonymousSendCommand.SaveSendAsync(send);
|
||||
return new SendResponseModel(send, _globalSettings);
|
||||
}
|
||||
|
||||
@ -175,15 +224,15 @@ public class SendsController : Controller
|
||||
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();
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var (send, data) = model.ToSend(userId, model.File.FileName, _sendService);
|
||||
var uploadUrl = await _sendService.SaveFileSendAsync(send, data, model.FileLength.Value);
|
||||
var (send, data) = model.ToSend(userId, model.File.FileName, _sendAuthorizationService);
|
||||
var uploadUrl = await _nonAnonymousSendCommand.SaveFileSendAsync(send, data, model.FileLength.Value);
|
||||
return new SendFileUploadDataResponseModel
|
||||
{
|
||||
Url = uploadUrl,
|
||||
@ -230,41 +279,7 @@ public class SendsController : Controller
|
||||
var send = await _sendRepository.GetByIdAsync(new Guid(id));
|
||||
await Request.GetFileAsync(async (stream) =>
|
||||
{
|
||||
await _sendService.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send);
|
||||
});
|
||||
}
|
||||
|
||||
@ -279,7 +294,7 @@ public class SendsController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _sendService.SaveSendAsync(model.ToSend(send, _sendService));
|
||||
await _nonAnonymousSendCommand.SaveSendAsync(model.ToSend(send, _sendAuthorizationService));
|
||||
return new SendResponseModel(send, _globalSettings);
|
||||
}
|
||||
|
||||
@ -294,7 +309,7 @@ public class SendsController : Controller
|
||||
}
|
||||
|
||||
send.Password = null;
|
||||
await _sendService.SaveSendAsync(send);
|
||||
await _nonAnonymousSendCommand.SaveSendAsync(send);
|
||||
return new SendResponseModel(send, _globalSettings);
|
||||
}
|
||||
|
||||
@ -308,6 +323,8 @@ public class SendsController : Controller
|
||||
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? HideEmail { get; set; }
|
||||
|
||||
public Send ToSend(Guid userId, ISendService sendService)
|
||||
public Send ToSend(Guid userId, ISendAuthorizationService sendAuthorizationService)
|
||||
{
|
||||
var send = new Send
|
||||
{
|
||||
Type = Type,
|
||||
UserId = (Guid?)userId
|
||||
};
|
||||
ToSend(send, sendService);
|
||||
ToSend(send, sendAuthorizationService);
|
||||
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
|
||||
{
|
||||
Type = Type,
|
||||
UserId = (Guid?)userId
|
||||
}, sendService);
|
||||
}, sendAuthorizationService);
|
||||
var data = new SendFileData(Name, Notes, fileName);
|
||||
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)
|
||||
{
|
||||
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.ExpirationDate = ExpirationDate;
|
||||
@ -133,7 +133,7 @@ public class SendRequestModel
|
||||
existingSend.MaxAccessCount = MaxAccessCount;
|
||||
if (!string.IsNullOrWhiteSpace(Password))
|
||||
{
|
||||
existingSend.Password = sendService.HashPassword(Password);
|
||||
existingSend.Password = authorizationService.HashPassword(Password);
|
||||
}
|
||||
existingSend.Disabled = Disabled.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);
|
||||
|
||||
// If we're not an "admin", we don't need to check the ciphers
|
||||
if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true }))
|
||||
// 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 }) || await _currentContext.ProviderUserForOrgAsync(organizationId))
|
||||
{
|
||||
// Are we a provider user? If so, we need to be sure we're not restricted
|
||||
// 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;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// If we're not an "admin", we don't need to check the ciphers
|
||||
if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true }))
|
||||
// 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 }) || await _currentContext.ProviderUserForOrgAsync(organizationId))
|
||||
{
|
||||
// Are we a provider user? If so, we need to be sure we're not restricted
|
||||
// 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;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Provider users can edit all ciphers if RestrictProviderAccess is disabled
|
||||
// Provider users cannot edit ciphers
|
||||
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
|
||||
{
|
||||
return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -485,10 +453,10 @@ public class CiphersController : Controller
|
||||
return true;
|
||||
}
|
||||
|
||||
// Provider users can only access organization ciphers if RestrictProviderAccess is disabled
|
||||
// Provider users cannot access organization ciphers
|
||||
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
|
||||
{
|
||||
return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -508,10 +476,10 @@ public class CiphersController : Controller
|
||||
return true;
|
||||
}
|
||||
|
||||
// Provider users can only access all ciphers if RestrictProviderAccess is disabled
|
||||
// Provider users cannot access ciphers
|
||||
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
|
||||
{
|
||||
return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -1086,9 +1054,8 @@ public class CiphersController : Controller
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization.
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||
// Check if the user is claimed by any organization.
|
||||
if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||
{
|
||||
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.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -37,6 +38,7 @@ public class SyncController : Controller
|
||||
private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion);
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
|
||||
public SyncController(
|
||||
IUserService userService,
|
||||
@ -51,7 +53,8 @@ public class SyncController : Controller
|
||||
GlobalSettings globalSettings,
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
IApplicationCacheService applicationCacheService)
|
||||
IApplicationCacheService applicationCacheService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
||||
{
|
||||
_userService = userService;
|
||||
_folderRepository = folderRepository;
|
||||
@ -66,6 +69,7 @@ public class SyncController : Controller
|
||||
_currentContext = currentContext;
|
||||
_featureService = featureService;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@ -102,7 +106,7 @@ public class SyncController : Controller
|
||||
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 organizationClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(user.Id);
|
||||
var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id);
|
||||
|
@ -63,6 +63,12 @@ public class FreshdeskController : Controller
|
||||
note += $"<li>Region: {_billingSettings.FreshDesk.Region}</li>";
|
||||
var customFields = new Dictionary<string, object>();
|
||||
var user = await _userRepository.GetByEmailAsync(ticketContactEmail);
|
||||
if (user == null)
|
||||
{
|
||||
note += $"<li>No user found: {ticketContactEmail}</li>";
|
||||
await CreateNote(ticketId, note);
|
||||
}
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
var userLink = $"{_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}";
|
||||
@ -121,18 +127,7 @@ public class FreshdeskController : Controller
|
||||
Content = JsonContent.Create(updateBody),
|
||||
};
|
||||
await CallFreshdeskApiAsync(updateRequest);
|
||||
|
||||
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);
|
||||
await CreateNote(ticketId, note);
|
||||
}
|
||||
|
||||
return new OkResult();
|
||||
@ -208,6 +203,21 @@ public class FreshdeskController : Controller
|
||||
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)
|
||||
{
|
||||
// if there is no content, then we don't need to add a note
|
||||
|
@ -1,8 +1,8 @@
|
||||
using Bit.Billing.Constants;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Entities;
|
||||
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.Repositories;
|
||||
using Stripe;
|
||||
|
@ -1,11 +1,11 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
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.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -25,8 +25,7 @@ public class UpcomingInvoiceHandler(
|
||||
IStripeEventService stripeEventService,
|
||||
IStripeEventUtilityService stripeEventUtilityService,
|
||||
IUserRepository userRepository,
|
||||
IValidateSponsorshipCommand validateSponsorshipCommand,
|
||||
IAutomaticTaxFactory automaticTaxFactory)
|
||||
IValidateSponsorshipCommand validateSponsorshipCommand)
|
||||
: IUpcomingInvoiceHandler
|
||||
{
|
||||
public async Task HandleAsync(Event parsedEvent)
|
||||
@ -46,6 +45,8 @@ public class UpcomingInvoiceHandler(
|
||||
|
||||
var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
|
||||
|
||||
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||
|
||||
if (organizationId.HasValue)
|
||||
{
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId.Value);
|
||||
@ -55,7 +56,7 @@ public class UpcomingInvoiceHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
await TryEnableAutomaticTaxAsync(subscription);
|
||||
await AlignOrganizationTaxConcernsAsync(organization, subscription, parsedEvent.Id, setNonUSBusinessUseToReverseCharge);
|
||||
|
||||
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
|
||||
@ -100,7 +101,25 @@ public class UpcomingInvoiceHandler(
|
||||
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)
|
||||
{
|
||||
@ -116,7 +135,7 @@ public class UpcomingInvoiceHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
await TryEnableAutomaticTaxAsync(subscription);
|
||||
await AlignProviderTaxConcernsAsync(provider, subscription, parsedEvent.Id, setNonUSBusinessUseToReverseCharge);
|
||||
|
||||
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 automaticTaxParameters = new AutomaticTaxFactoryParameters(subscription.Items.Select(x => x.Price.Id));
|
||||
var automaticTaxStrategy = await automaticTaxFactory.CreateAsync(automaticTaxParameters);
|
||||
var updateOptions = automaticTaxStrategy.GetUpdateOptions(subscription);
|
||||
var nonUSBusinessUse =
|
||||
organization.PlanType.GetProductTier() != ProductTierType.Families &&
|
||||
subscription.Customer.Address.Country != "US";
|
||||
|
||||
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);
|
||||
return;
|
||||
setAutomaticTaxToEnabled = true;
|
||||
}
|
||||
|
||||
if (subscription.AutomaticTax.Enabled ||
|
||||
!subscription.Customer.HasBillingLocation() ||
|
||||
await IsNonTaxableNonUSBusinessUseSubscription(subscription))
|
||||
else
|
||||
{
|
||||
return;
|
||||
setAutomaticTaxToEnabled =
|
||||
subscription.Customer.HasRecognizedTaxLocation() &&
|
||||
(subscription.Customer.Address.Country == "US" ||
|
||||
(nonUSBusinessUse && subscription.Customer.TaxIds.Any()));
|
||||
}
|
||||
|
||||
await stripeFacade.UpdateSubscription(subscription.Id,
|
||||
new SubscriptionUpdateOptions
|
||||
if (!subscription.AutomaticTax.Enabled && setAutomaticTaxToEnabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
DefaultTaxRates = [],
|
||||
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
||||
});
|
||||
await stripeFacade.UpdateSubscription(subscription.Id,
|
||||
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(
|
||||
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019),
|
||||
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually)))
|
||||
.Select(plan => plan.PasswordManager.StripePlanId);
|
||||
if (subscription.Customer.Address.Country != "US" && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
|
||||
{
|
||||
try
|
||||
{
|
||||
await stripeFacade.UpdateCustomer(subscription.CustomerId,
|
||||
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(
|
||||
exception,
|
||||
"Failed to set provider's ({ProviderID}) to reverse tax exemption while processing event with ID {EventID}",
|
||||
provider.Id,
|
||||
eventId);
|
||||
}
|
||||
}
|
||||
|
||||
return localSubscription.Customer.Address.Country != "US" &&
|
||||
localSubscription.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId) &&
|
||||
!localSubscription.Items.Select(item => item.Price.Id).Intersect(familyPriceIds).Any() &&
|
||||
!localSubscription.Customer.TaxIds.Any();
|
||||
setAutomaticTaxToEnabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
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>
|
||||
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>
|
||||
/// If set to true, admins can initiate organization-issued sponsorships.
|
||||
/// </summary>
|
||||
@ -319,5 +324,7 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
||||
SmSeats = license.SmSeats;
|
||||
SmServiceAccounts = license.SmServiceAccounts;
|
||||
UseRiskInsights = license.UseRiskInsights;
|
||||
UseOrganizationDomains = license.UseOrganizationDomains;
|
||||
UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies;
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ public class OrganizationAbility
|
||||
LimitItemDeletion = organization.LimitItemDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
UseRiskInsights = organization.UseRiskInsights;
|
||||
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||
}
|
||||
|
||||
@ -46,5 +47,6 @@ public class OrganizationAbility
|
||||
public bool LimitItemDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseOrganizationDomains { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
}
|
||||
|
@ -59,5 +59,7 @@ public class OrganizationUserOrganizationDetails
|
||||
public bool LimitItemDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseOrganizationDomains { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public bool? IsAdminInitiated { get; set; }
|
||||
}
|
||||
|
@ -150,6 +150,7 @@ public class SelfHostedOrganizationDetails : Organization
|
||||
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
|
||||
Status = Status,
|
||||
UseRiskInsights = UseRiskInsights,
|
||||
UseAdminSponsoredFamilies = UseAdminSponsoredFamilies,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ public class ProviderUserOrganizationDetails
|
||||
public bool LimitItemDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseOrganizationDomains { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public ProviderType ProviderType { get; set; }
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ public class VerifyOrganizationDomainCommand(
|
||||
IDnsResolverService dnsResolverService,
|
||||
IEventService eventService,
|
||||
IGlobalSettings globalSettings,
|
||||
IFeatureService featureService,
|
||||
ICurrentContext currentContext,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IMailService mailService,
|
||||
@ -125,11 +124,8 @@ public class VerifyOrganizationDomainCommand(
|
||||
|
||||
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) =>
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -24,6 +25,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||
|
||||
public AcceptOrgUserCommand(
|
||||
@ -34,6 +36,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
||||
IPolicyService policyService,
|
||||
IMailService mailService,
|
||||
IUserRepository userRepository,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
|
||||
{
|
||||
|
||||
@ -45,6 +48,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
||||
_policyService = policyService;
|
||||
_mailService = mailService;
|
||||
_userRepository = userRepository;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||
}
|
||||
|
||||
@ -192,7 +196,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
||||
}
|
||||
|
||||
// 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,
|
||||
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
|
||||
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
|
||||
|
||||
// TODO: Replace "UseSso" with a new organization ability like "UseOrganizationDomains" (PM-11622).
|
||||
// Verified domains were tied to SSO, so we currently check the "UseSso" organization ability.
|
||||
if (organizationAbility is { Enabled: true, UseSso: true })
|
||||
if (organizationAbility is { Enabled: true, UseOrganizationDomains: true })
|
||||
{
|
||||
// Get all organization users with claimed domains by the organization
|
||||
var organizationUsersWithClaimedDomain = await _organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organizationId);
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
|
||||
|
@ -6,4 +6,8 @@ namespace Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
public interface IOrganizationUserUserDetailsQuery
|
||||
{
|
||||
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