mirror of
https://github.com/bitwarden/server.git
synced 2025-05-13 23:52:15 -05:00
Merge branch 'main' into add-docker-arm64-builds
This commit is contained in:
commit
3eb40f7773
8
.github/CODEOWNERS
vendored
8
.github/CODEOWNERS
vendored
@ -43,8 +43,16 @@ src/Core/IdentityServer @bitwarden/team-auth-dev
|
|||||||
# Key Management team
|
# Key Management team
|
||||||
**/KeyManagement @bitwarden/team-key-management-dev
|
**/KeyManagement @bitwarden/team-key-management-dev
|
||||||
|
|
||||||
|
# Tools team
|
||||||
**/Tools @bitwarden/team-tools-dev
|
**/Tools @bitwarden/team-tools-dev
|
||||||
|
|
||||||
|
# Dirt (Data Insights & Reporting) team
|
||||||
|
src/Api/Controllers/Dirt @bitwarden/team-data-insights-and-reporting-dev
|
||||||
|
src/Core/Dirt @bitwarden/team-data-insights-and-reporting-dev
|
||||||
|
src/Infrastructure.Dapper/Dirt @bitwarden/team-data-insights-and-reporting-dev
|
||||||
|
test/Api.Test/Dirt @bitwarden/team-data-insights-and-reporting-dev
|
||||||
|
test/Core.Test/Dirt @bitwarden/team-data-insights-and-reporting-dev
|
||||||
|
|
||||||
# Vault team
|
# Vault team
|
||||||
**/Vault @bitwarden/team-vault-dev
|
**/Vault @bitwarden/team-vault-dev
|
||||||
**/Vault/AuthorizationHandlers @bitwarden/team-vault-dev @bitwarden/team-admin-console-dev # joint ownership over authorization handlers that affect organization users
|
**/Vault/AuthorizationHandlers @bitwarden/team-vault-dev @bitwarden/team-admin-console-dev # joint ownership over authorization handlers that affect organization users
|
||||||
|
66
.github/renovate.json5
vendored
66
.github/renovate.json5
vendored
@ -20,7 +20,7 @@
|
|||||||
],
|
],
|
||||||
commitMessagePrefix: "[deps] BRE:",
|
commitMessagePrefix: "[deps] BRE:",
|
||||||
reviewers: ["team:dept-bre"],
|
reviewers: ["team:dept-bre"],
|
||||||
addLabels: ["hold"]
|
addLabels: ["hold"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
groupName: "dockerfile minor",
|
groupName: "dockerfile minor",
|
||||||
@ -37,6 +37,16 @@
|
|||||||
matchManagers: ["github-actions"],
|
matchManagers: ["github-actions"],
|
||||||
matchUpdateTypes: ["minor"],
|
matchUpdateTypes: ["minor"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// For any Microsoft.Extensions.* and Microsoft.AspNetCore.* packages, we want to create PRs for patch updates.
|
||||||
|
// This overrides the default that ignores patch updates for nuget dependencies.
|
||||||
|
matchPackageNames: [
|
||||||
|
"/^Microsoft\\.Extensions\\./",
|
||||||
|
"/^Microsoft\\.AspNetCore\\./",
|
||||||
|
],
|
||||||
|
matchUpdateTypes: ["patch"],
|
||||||
|
dependencyDashboardApproval: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
matchManagers: ["dockerfile", "docker-compose"],
|
matchManagers: ["dockerfile", "docker-compose"],
|
||||||
commitMessagePrefix: "[deps] BRE:",
|
commitMessagePrefix: "[deps] BRE:",
|
||||||
@ -59,6 +69,7 @@
|
|||||||
"DuoUniversal",
|
"DuoUniversal",
|
||||||
"Fido2.AspNet",
|
"Fido2.AspNet",
|
||||||
"Duende.IdentityServer",
|
"Duende.IdentityServer",
|
||||||
|
"Microsoft.AspNetCore.Authentication.JwtBearer",
|
||||||
"Microsoft.Extensions.Identity.Stores",
|
"Microsoft.Extensions.Identity.Stores",
|
||||||
"Otp.NET",
|
"Otp.NET",
|
||||||
"Sustainsys.Saml2.AspNetCore2",
|
"Sustainsys.Saml2.AspNetCore2",
|
||||||
@ -79,8 +90,6 @@
|
|||||||
"CsvHelper",
|
"CsvHelper",
|
||||||
"Kralizek.AutoFixture.Extensions.MockHttp",
|
"Kralizek.AutoFixture.Extensions.MockHttp",
|
||||||
"Microsoft.AspNetCore.Mvc.Testing",
|
"Microsoft.AspNetCore.Mvc.Testing",
|
||||||
"Microsoft.Extensions.Logging",
|
|
||||||
"Microsoft.Extensions.Logging.Console",
|
|
||||||
"Newtonsoft.Json",
|
"Newtonsoft.Json",
|
||||||
"NSubstitute",
|
"NSubstitute",
|
||||||
"Sentry.Serilog",
|
"Sentry.Serilog",
|
||||||
@ -100,9 +109,9 @@
|
|||||||
reviewers: ["team:team-billing-dev"],
|
reviewers: ["team:team-billing-dev"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
matchPackagePatterns: ["^Microsoft.Extensions.Logging"],
|
matchPackageNames: ["/^Microsoft\\.EntityFrameworkCore\\./", "/^dotnet-ef/"],
|
||||||
groupName: "Microsoft.Extensions.Logging",
|
groupName: "EntityFrameworkCore",
|
||||||
description: "Group Microsoft.Extensions.Logging to exclude them from the dotnet monorepo preset",
|
description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
matchPackageNames: [
|
matchPackageNames: [
|
||||||
@ -117,9 +126,6 @@
|
|||||||
"Microsoft.EntityFrameworkCore.Relational",
|
"Microsoft.EntityFrameworkCore.Relational",
|
||||||
"Microsoft.EntityFrameworkCore.Sqlite",
|
"Microsoft.EntityFrameworkCore.Sqlite",
|
||||||
"Microsoft.EntityFrameworkCore.SqlServer",
|
"Microsoft.EntityFrameworkCore.SqlServer",
|
||||||
"Microsoft.Extensions.Caching.Cosmos",
|
|
||||||
"Microsoft.Extensions.Caching.SqlServer",
|
|
||||||
"Microsoft.Extensions.Caching.StackExchangeRedis",
|
|
||||||
"Npgsql.EntityFrameworkCore.PostgreSQL",
|
"Npgsql.EntityFrameworkCore.PostgreSQL",
|
||||||
"Pomelo.EntityFrameworkCore.MySql",
|
"Pomelo.EntityFrameworkCore.MySql",
|
||||||
],
|
],
|
||||||
@ -142,56 +148,40 @@
|
|||||||
"Azure.Messaging.ServiceBus",
|
"Azure.Messaging.ServiceBus",
|
||||||
"Azure.Storage.Blobs",
|
"Azure.Storage.Blobs",
|
||||||
"Azure.Storage.Queues",
|
"Azure.Storage.Queues",
|
||||||
"Microsoft.AspNetCore.Authentication.JwtBearer",
|
"LaunchDarkly.ServerSdk",
|
||||||
"Microsoft.AspNetCore.Http",
|
"Microsoft.AspNetCore.Http",
|
||||||
|
"Microsoft.AspNetCore.SignalR.Protocols.MessagePack",
|
||||||
|
"Microsoft.AspNetCore.SignalR.StackExchangeRedis",
|
||||||
|
"Microsoft.Extensions.Configuration.EnvironmentVariables",
|
||||||
|
"Microsoft.Extensions.Configuration.UserSecrets",
|
||||||
|
"Microsoft.Extensions.Configuration",
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions",
|
||||||
|
"Microsoft.Extensions.DependencyInjection",
|
||||||
|
"Microsoft.Extensions.Logging",
|
||||||
|
"Microsoft.Extensions.Logging.Console",
|
||||||
|
"Microsoft.Extensions.Caching.Cosmos",
|
||||||
|
"Microsoft.Extensions.Caching.SqlServer",
|
||||||
|
"Microsoft.Extensions.Caching.StackExchangeRedis",
|
||||||
"Quartz",
|
"Quartz",
|
||||||
],
|
],
|
||||||
description: "Platform owned dependencies",
|
description: "Platform owned dependencies",
|
||||||
commitMessagePrefix: "[deps] Platform:",
|
commitMessagePrefix: "[deps] Platform:",
|
||||||
reviewers: ["team:team-platform-dev"],
|
reviewers: ["team:team-platform-dev"],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
matchPackagePatterns: ["EntityFrameworkCore", "^dotnet-ef"],
|
|
||||||
groupName: "EntityFrameworkCore",
|
|
||||||
description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
matchPackageNames: [
|
matchPackageNames: [
|
||||||
"AutoMapper.Extensions.Microsoft.DependencyInjection",
|
"AutoMapper.Extensions.Microsoft.DependencyInjection",
|
||||||
"AWSSDK.SimpleEmail",
|
"AWSSDK.SimpleEmail",
|
||||||
"AWSSDK.SQS",
|
"AWSSDK.SQS",
|
||||||
"Handlebars.Net",
|
"Handlebars.Net",
|
||||||
"LaunchDarkly.ServerSdk",
|
|
||||||
"MailKit",
|
"MailKit",
|
||||||
"Microsoft.AspNetCore.SignalR.Protocols.MessagePack",
|
|
||||||
"Microsoft.AspNetCore.SignalR.StackExchangeRedis",
|
|
||||||
"Microsoft.Azure.NotificationHubs",
|
"Microsoft.Azure.NotificationHubs",
|
||||||
"Microsoft.Extensions.Configuration.EnvironmentVariables",
|
|
||||||
"Microsoft.Extensions.Configuration.UserSecrets",
|
|
||||||
"Microsoft.Extensions.Configuration",
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions",
|
|
||||||
"Microsoft.Extensions.DependencyInjection",
|
|
||||||
"SendGrid",
|
"SendGrid",
|
||||||
],
|
],
|
||||||
description: "Tools owned dependencies",
|
description: "Tools owned dependencies",
|
||||||
commitMessagePrefix: "[deps] Tools:",
|
commitMessagePrefix: "[deps] Tools:",
|
||||||
reviewers: ["team:team-tools-dev"],
|
reviewers: ["team:team-tools-dev"],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
matchPackagePatterns: ["^Microsoft.AspNetCore.SignalR"],
|
|
||||||
groupName: "SignalR",
|
|
||||||
description: "Group SignalR to exclude them from the dotnet monorepo preset",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
matchPackagePatterns: ["^Microsoft.Extensions.Configuration"],
|
|
||||||
groupName: "Microsoft.Extensions.Configuration",
|
|
||||||
description: "Group Microsoft.Extensions.Configuration to exclude them from the dotnet monorepo preset",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
matchPackagePatterns: ["^Microsoft.Extensions.DependencyInjection"],
|
|
||||||
groupName: "Microsoft.Extensions.DependencyInjection",
|
|
||||||
description: "Group Microsoft.Extensions.DependencyInjection to exclude them from the dotnet monorepo preset",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
matchPackageNames: [
|
matchPackageNames: [
|
||||||
"AngleSharp",
|
"AngleSharp",
|
||||||
|
11
.github/workflows/build.yml
vendored
11
.github/workflows/build.yml
vendored
@ -14,6 +14,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
||||||
|
_GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
@ -128,12 +129,18 @@ jobs:
|
|||||||
- name: Generate container image tag
|
- name: Generate container image tag
|
||||||
id: tag
|
id: tag
|
||||||
run: |
|
run: |
|
||||||
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
|
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" || "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then
|
||||||
IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s#/#-#g")
|
IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize branch name to alphanumeric only
|
||||||
else
|
else
|
||||||
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||||
|
SANITIZED_REPO_NAME=$(echo "$_GITHUB_PR_REPO_NAME" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize repo name to alphanumeric only
|
||||||
|
IMAGE_TAG=$SANITIZED_REPO_NAME-$IMAGE_TAG # Add repo name to the tag
|
||||||
|
IMAGE_TAG=${IMAGE_TAG:0:128} # Limit to 128 characters, as that's the max length for Docker image tags
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$IMAGE_TAG" == "main" ]]; then
|
if [[ "$IMAGE_TAG" == "main" ]]; then
|
||||||
IMAGE_TAG=dev
|
IMAGE_TAG=dev
|
||||||
fi
|
fi
|
||||||
|
15
.github/workflows/code-references.yml
vendored
15
.github/workflows/code-references.yml
vendored
@ -1,7 +1,10 @@
|
|||||||
name: Collect code references
|
name: Collect code references
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
push:
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-ld-secret:
|
check-ld-secret:
|
||||||
@ -37,12 +40,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Collect
|
- name: Collect
|
||||||
id: collect
|
id: collect
|
||||||
uses: launchdarkly/find-code-references-in-pull-request@30f4c4ab2949bbf258b797ced2fbf6dea34df9ce # v2.1.0
|
uses: launchdarkly/find-code-references@e3e9da201b87ada54eb4c550c14fb783385c5c8a # v2.13.0
|
||||||
with:
|
with:
|
||||||
project-key: default
|
accessToken: ${{ secrets.LD_ACCESS_TOKEN }}
|
||||||
environment-key: dev
|
projKey: default
|
||||||
access-token: ${{ secrets.LD_ACCESS_TOKEN }}
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Add label
|
- name: Add label
|
||||||
if: steps.collect.outputs.any-changed == 'true'
|
if: steps.collect.outputs.any-changed == 'true'
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
<Version>2025.4.3</Version>
|
<Version>2025.5.0</Version>
|
||||||
|
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
@ -129,6 +129,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "t
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.IntegrationTest", "test\Core.IntegrationTest\Core.IntegrationTest.csproj", "{3631BA42-6731-4118-A917-DAA43C5032B9}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.IntegrationTest", "test\Core.IntegrationTest\Core.IntegrationTest.csproj", "{3631BA42-6731-4118-A917-DAA43C5032B9}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seeder", "util\Seeder\Seeder.csproj", "{9A612EBA-1C0E-42B8-982B-62F0EE81000A}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -325,6 +329,14 @@ Global
|
|||||||
{3631BA42-6731-4118-A917-DAA43C5032B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{3631BA42-6731-4118-A917-DAA43C5032B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.Build.0 = Release|Any CPU
|
{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -377,6 +389,8 @@ Global
|
|||||||
{4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
{3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
|
{9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||||
|
{17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||||
|
@ -8,7 +8,8 @@ using Bit.Core.Billing.Constants;
|
|||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
using Bit.Core.Billing.Tax.Services;
|
||||||
|
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -82,7 +83,7 @@ public class ProviderService : IProviderService
|
|||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo = null)
|
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null)
|
||||||
{
|
{
|
||||||
var owner = await _userService.GetUserByIdAsync(ownerUserId);
|
var owner = await _userService.GetUserByIdAsync(ownerUserId);
|
||||||
if (owner == null)
|
if (owner == null)
|
||||||
@ -111,7 +112,20 @@ public class ProviderService : IProviderService
|
|||||||
{
|
{
|
||||||
throw new BadRequestException("Both address and postal code are required to set up your provider.");
|
throw new BadRequestException("Both address and postal code are required to set up your provider.");
|
||||||
}
|
}
|
||||||
var customer = await _providerBillingService.SetupCustomer(provider, taxInfo);
|
|
||||||
|
var requireProviderPaymentMethodDuringSetup =
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup);
|
||||||
|
|
||||||
|
if (requireProviderPaymentMethodDuringSetup && tokenizedPaymentSource is not
|
||||||
|
{
|
||||||
|
Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal,
|
||||||
|
Token: not null and not ""
|
||||||
|
})
|
||||||
|
{
|
||||||
|
throw new BadRequestException("A payment method is required to set up your provider.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var customer = await _providerBillingService.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
|
||||||
provider.GatewayCustomerId = customer.Id;
|
provider.GatewayCustomerId = customer.Id;
|
||||||
var subscription = await _providerBillingService.SetupSubscription(provider);
|
var subscription = await _providerBillingService.SetupSubscription(provider);
|
||||||
provider.GatewaySubscriptionId = subscription.Id;
|
provider.GatewaySubscriptionId = subscription.Id;
|
||||||
|
@ -6,29 +6,39 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing;
|
using Bit.Core.Billing;
|
||||||
|
using Bit.Core.Billing.Caches;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Entities;
|
using Bit.Core.Billing.Entities;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
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.Billing.Tax.Services.Implementations;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
|
using Braintree;
|
||||||
using CsvHelper;
|
using CsvHelper;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Stripe;
|
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;
|
||||||
|
|
||||||
public class ProviderBillingService(
|
public class ProviderBillingService(
|
||||||
|
IBraintreeGateway braintreeGateway,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
@ -39,6 +49,7 @@ public class ProviderBillingService(
|
|||||||
IProviderOrganizationRepository providerOrganizationRepository,
|
IProviderOrganizationRepository providerOrganizationRepository,
|
||||||
IProviderPlanRepository providerPlanRepository,
|
IProviderPlanRepository providerPlanRepository,
|
||||||
IProviderUserRepository providerUserRepository,
|
IProviderUserRepository providerUserRepository,
|
||||||
|
ISetupIntentCache setupIntentCache,
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
ISubscriberService subscriberService,
|
ISubscriberService subscriberService,
|
||||||
ITaxService taxService,
|
ITaxService taxService,
|
||||||
@ -463,7 +474,8 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
public async Task<Customer> SetupCustomer(
|
public async Task<Customer> SetupCustomer(
|
||||||
Provider provider,
|
Provider provider,
|
||||||
TaxInfo taxInfo)
|
TaxInfo taxInfo,
|
||||||
|
TokenizedPaymentSource tokenizedPaymentSource = null)
|
||||||
{
|
{
|
||||||
if (taxInfo is not
|
if (taxInfo is not
|
||||||
{
|
{
|
||||||
@ -532,13 +544,97 @@ public class ProviderBillingService(
|
|||||||
options.Coupon = provider.DiscountId;
|
options.Coupon = provider.DiscountId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var requireProviderPaymentMethodDuringSetup =
|
||||||
|
featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup);
|
||||||
|
|
||||||
|
var braintreeCustomerId = "";
|
||||||
|
|
||||||
|
if (requireProviderPaymentMethodDuringSetup)
|
||||||
|
{
|
||||||
|
if (tokenizedPaymentSource is not
|
||||||
|
{
|
||||||
|
Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal,
|
||||||
|
Token: not null and not ""
|
||||||
|
})
|
||||||
|
{
|
||||||
|
logger.LogError("Cannot create customer for provider ({ProviderID}) without a payment method", provider.Id);
|
||||||
|
throw new BillingException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var (type, token) = tokenizedPaymentSource;
|
||||||
|
|
||||||
|
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case PaymentMethodType.BankAccount:
|
||||||
|
{
|
||||||
|
var setupIntent =
|
||||||
|
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = token }))
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (setupIntent == null)
|
||||||
|
{
|
||||||
|
logger.LogError("Cannot create customer for provider ({ProviderID}) without a setup intent for their bank account", provider.Id);
|
||||||
|
throw new BillingException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await setupIntentCache.Set(provider.Id, setupIntent.Id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PaymentMethodType.Card:
|
||||||
|
{
|
||||||
|
options.PaymentMethod = token;
|
||||||
|
options.InvoiceSettings.DefaultPaymentMethod = token;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PaymentMethodType.PayPal:
|
||||||
|
{
|
||||||
|
braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(provider, token);
|
||||||
|
options.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await stripeAdapter.CustomerCreateAsync(options);
|
return await stripeAdapter.CustomerCreateAsync(options);
|
||||||
}
|
}
|
||||||
catch (StripeException stripeException) when (stripeException.StripeError?.Code == StripeConstants.ErrorCodes.TaxIdInvalid)
|
catch (StripeException stripeException) when (stripeException.StripeError?.Code ==
|
||||||
|
StripeConstants.ErrorCodes.TaxIdInvalid)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Your tax ID wasn't recognized for your selected country. Please ensure your country and tax ID are valid.");
|
await Revert();
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Your tax ID wasn't recognized for your selected country. Please ensure your country and tax ID are valid.");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await Revert();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task Revert()
|
||||||
|
{
|
||||||
|
if (requireProviderPaymentMethodDuringSetup && tokenizedPaymentSource != null)
|
||||||
|
{
|
||||||
|
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||||
|
switch (tokenizedPaymentSource.Type)
|
||||||
|
{
|
||||||
|
case PaymentMethodType.BankAccount:
|
||||||
|
{
|
||||||
|
var setupIntentId = await setupIntentCache.Get(provider.Id);
|
||||||
|
await stripeAdapter.SetupIntentCancel(setupIntentId,
|
||||||
|
new SetupIntentCancelOptions { CancellationReason = "abandoned" });
|
||||||
|
await setupIntentCache.Remove(provider.Id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
|
||||||
|
{
|
||||||
|
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -580,18 +676,38 @@ public class ProviderBillingService(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var requireProviderPaymentMethodDuringSetup =
|
||||||
|
featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup);
|
||||||
|
|
||||||
|
var setupIntentId = await setupIntentCache.Get(provider.Id);
|
||||||
|
|
||||||
|
var setupIntent = !string.IsNullOrEmpty(setupIntentId)
|
||||||
|
? await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions
|
||||||
|
{
|
||||||
|
Expand = ["payment_method"]
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
var usePaymentMethod =
|
||||||
|
requireProviderPaymentMethodDuringSetup &&
|
||||||
|
(!string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) ||
|
||||||
|
customer.Metadata.ContainsKey(BraintreeCustomerIdKey) ||
|
||||||
|
setupIntent.IsUnverifiedBankAccount());
|
||||||
|
|
||||||
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
||||||
{
|
{
|
||||||
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
|
CollectionMethod = usePaymentMethod ?
|
||||||
|
StripeConstants.CollectionMethod.ChargeAutomatically : StripeConstants.CollectionMethod.SendInvoice,
|
||||||
Customer = customer.Id,
|
Customer = customer.Id,
|
||||||
DaysUntilDue = 30,
|
DaysUntilDue = usePaymentMethod ? null : 30,
|
||||||
Items = subscriptionItemOptionsList,
|
Items = subscriptionItemOptionsList,
|
||||||
Metadata = new Dictionary<string, string>
|
Metadata = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
{ "providerId", provider.Id.ToString() }
|
{ "providerId", provider.Id.ToString() }
|
||||||
},
|
},
|
||||||
OffSession = true,
|
OffSession = true,
|
||||||
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations
|
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
|
||||||
|
TrialPeriodDays = usePaymentMethod ? 14 : null
|
||||||
};
|
};
|
||||||
|
|
||||||
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
|
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
|
||||||
@ -607,7 +723,10 @@ public class ProviderBillingService(
|
|||||||
{
|
{
|
||||||
var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||||
|
|
||||||
if (subscription.Status == StripeConstants.SubscriptionStatus.Active)
|
if (subscription is
|
||||||
|
{
|
||||||
|
Status: StripeConstants.SubscriptionStatus.Active or StripeConstants.SubscriptionStatus.Trialing
|
||||||
|
})
|
||||||
{
|
{
|
||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Settings;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
namespace Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
||||||
|
|
||||||
@ -11,36 +12,43 @@ public class MaxProjectsQuery : IMaxProjectsQuery
|
|||||||
{
|
{
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IProjectRepository _projectRepository;
|
private readonly IProjectRepository _projectRepository;
|
||||||
|
private readonly IGlobalSettings _globalSettings;
|
||||||
|
private readonly IPricingClient _pricingClient;
|
||||||
|
|
||||||
public MaxProjectsQuery(
|
public MaxProjectsQuery(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IProjectRepository projectRepository)
|
IProjectRepository projectRepository,
|
||||||
|
IGlobalSettings globalSettings,
|
||||||
|
IPricingClient pricingClient)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_projectRepository = projectRepository;
|
_projectRepository = projectRepository;
|
||||||
|
_globalSettings = globalSettings;
|
||||||
|
_pricingClient = pricingClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(short? max, bool? overMax)> GetByOrgIdAsync(Guid organizationId, int projectsToAdd)
|
public async Task<(short? max, bool? overMax)> GetByOrgIdAsync(Guid organizationId, int projectsToAdd)
|
||||||
{
|
{
|
||||||
|
// "MaxProjects" only applies to free 2-person organizations, which can't be self-hosted.
|
||||||
|
if (_globalSettings.SelfHosted)
|
||||||
|
{
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||||
if (org == null)
|
if (org == null)
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: PRICING -> https://bitwarden.atlassian.net/browse/PM-17122
|
var plan = await _pricingClient.GetPlan(org.PlanType);
|
||||||
var plan = StaticStore.GetPlan(org.PlanType);
|
|
||||||
if (plan?.SecretsManager == null)
|
if (plan is not { SecretsManager: not null, Type: PlanType.Free })
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Existing plan not found.");
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plan.Type == PlanType.Free)
|
var projects = await _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId);
|
||||||
{
|
return ((short? max, bool? overMax))(projects + projectsToAdd > plan.SecretsManager.MaxProjects ? (plan.SecretsManager.MaxProjects, true) : (plan.SecretsManager.MaxProjects, false));
|
||||||
var projects = await _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId);
|
|
||||||
return ((short? max, bool? overMax))(projects + projectsToAdd > plan.SecretsManager.MaxProjects ? (plan.SecretsManager.MaxProjects, true) : (plan.SecretsManager.MaxProjects, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (null, null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,10 @@ using Bit.Core.AdminConsole.Models.Business;
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
|
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Commands;
|
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -76,9 +76,8 @@ public class PostUserCommand(
|
|||||||
var invitedOrganizationUserId = result switch
|
var invitedOrganizationUserId = result switch
|
||||||
{
|
{
|
||||||
Success<ScimInviteOrganizationUsersResponse> success => success.Value.InvitedUser.Id,
|
Success<ScimInviteOrganizationUsersResponse> success => success.Value.InvitedUser.Id,
|
||||||
Failure<ScimInviteOrganizationUsersResponse> failure when failure.Errors
|
Failure<ScimInviteOrganizationUsersResponse> { Error.Message: NoUsersToInviteError.Code } => (Guid?)null,
|
||||||
.Any(x => x.Message == NoUsersToInviteError.Code) => (Guid?)null,
|
Failure<ScimInviteOrganizationUsersResponse> failure => throw MapToBitException(failure.Error),
|
||||||
Failure<ScimInviteOrganizationUsersResponse> failure when failure.Errors.Length != 0 => throw MapToBitException(failure.Errors),
|
|
||||||
_ => throw new InvalidOperationException()
|
_ => throw new InvalidOperationException()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ using Bit.Core.Billing.Constants;
|
|||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Tax.Services;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using Bit.Commercial.Core.AdminConsole.Services;
|
using Bit.Commercial.Core.AdminConsole.Services;
|
||||||
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
|
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
@ -7,6 +8,7 @@ using Bit.Core.AdminConsole.Models.Business.Provider;
|
|||||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -38,7 +40,7 @@ public class ProviderServiceTests
|
|||||||
public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
|
public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
|
||||||
{
|
{
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.CompleteSetupAsync(default, default, default, default));
|
() => sutProvider.Sut.CompleteSetupAsync(default, default, default, default, null));
|
||||||
Assert.Contains("Invalid owner.", exception.Message);
|
Assert.Contains("Invalid owner.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,12 +52,85 @@ public class ProviderServiceTests
|
|||||||
userService.GetUserByIdAsync(user.Id).Returns(user);
|
userService.GetUserByIdAsync(user.Id).Returns(user);
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default));
|
() => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default, null));
|
||||||
Assert.Contains("Invalid token.", exception.Message);
|
Assert.Contains("Invalid token.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TaxInfo taxInfo,
|
public async Task CompleteSetupAsync_InvalidTaxInfo_ThrowsBadRequestException(
|
||||||
|
User user,
|
||||||
|
Provider provider,
|
||||||
|
string key,
|
||||||
|
TaxInfo taxInfo,
|
||||||
|
TokenizedPaymentSource tokenizedPaymentSource,
|
||||||
|
[ProviderUser] ProviderUser providerUser,
|
||||||
|
SutProvider<ProviderService> sutProvider)
|
||||||
|
{
|
||||||
|
providerUser.ProviderId = provider.Id;
|
||||||
|
providerUser.UserId = user.Id;
|
||||||
|
var userService = sutProvider.GetDependency<IUserService>();
|
||||||
|
userService.GetUserByIdAsync(user.Id).Returns(user);
|
||||||
|
|
||||||
|
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||||
|
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
|
||||||
|
|
||||||
|
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
|
||||||
|
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
||||||
|
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
|
||||||
|
.Returns(protector);
|
||||||
|
|
||||||
|
sutProvider.Create();
|
||||||
|
|
||||||
|
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||||
|
|
||||||
|
taxInfo.BillingAddressCountry = null;
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
|
sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource));
|
||||||
|
|
||||||
|
Assert.Equal("Both address and postal code are required to set up your provider.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task CompleteSetupAsync_InvalidTokenizedPaymentSource_ThrowsBadRequestException(
|
||||||
|
User user,
|
||||||
|
Provider provider,
|
||||||
|
string key,
|
||||||
|
TaxInfo taxInfo,
|
||||||
|
TokenizedPaymentSource tokenizedPaymentSource,
|
||||||
|
[ProviderUser] ProviderUser providerUser,
|
||||||
|
SutProvider<ProviderService> sutProvider)
|
||||||
|
{
|
||||||
|
providerUser.ProviderId = provider.Id;
|
||||||
|
providerUser.UserId = user.Id;
|
||||||
|
var userService = sutProvider.GetDependency<IUserService>();
|
||||||
|
userService.GetUserByIdAsync(user.Id).Returns(user);
|
||||||
|
|
||||||
|
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||||
|
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
|
||||||
|
|
||||||
|
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
|
||||||
|
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
||||||
|
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
|
||||||
|
.Returns(protector);
|
||||||
|
|
||||||
|
sutProvider.Create();
|
||||||
|
|
||||||
|
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||||
|
|
||||||
|
tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay };
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
|
sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource));
|
||||||
|
|
||||||
|
Assert.Equal("A payment method is required to set up your provider.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource,
|
||||||
[ProviderUser] ProviderUser providerUser,
|
[ProviderUser] ProviderUser providerUser,
|
||||||
SutProvider<ProviderService> sutProvider)
|
SutProvider<ProviderService> sutProvider)
|
||||||
{
|
{
|
||||||
@ -75,7 +150,7 @@ public class ProviderServiceTests
|
|||||||
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
|
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
|
||||||
|
|
||||||
var customer = new Customer { Id = "customer_id" };
|
var customer = new Customer { Id = "customer_id" };
|
||||||
providerBillingService.SetupCustomer(provider, taxInfo).Returns(customer);
|
providerBillingService.SetupCustomer(provider, taxInfo, tokenizedPaymentSource).Returns(customer);
|
||||||
|
|
||||||
var subscription = new Subscription { Id = "subscription_id" };
|
var subscription = new Subscription { Id = "subscription_id" };
|
||||||
providerBillingService.SetupSubscription(provider).Returns(subscription);
|
providerBillingService.SetupSubscription(provider).Returns(subscription);
|
||||||
@ -84,7 +159,7 @@ public class ProviderServiceTests
|
|||||||
|
|
||||||
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||||
|
|
||||||
await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo);
|
await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IProviderRepository>().Received().UpsertAsync(Arg.Is<Provider>(
|
await sutProvider.GetDependency<IProviderRepository>().Received().UpsertAsync(Arg.Is<Provider>(
|
||||||
p =>
|
p =>
|
||||||
|
@ -2,18 +2,22 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using Bit.Commercial.Core.Billing;
|
using Bit.Commercial.Core.Billing;
|
||||||
using Bit.Commercial.Core.Billing.Models;
|
using Bit.Commercial.Core.Billing.Models;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Billing.Caches;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Entities;
|
using Bit.Core.Billing.Entities;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
|
using Bit.Core.Billing.Tax.Services;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -24,11 +28,17 @@ using Bit.Core.Settings;
|
|||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Braintree;
|
||||||
using CsvHelper;
|
using CsvHelper;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
|
using NSubstitute.ExceptionExtensions;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using static Bit.Core.Test.Billing.Utilities;
|
using static Bit.Core.Test.Billing.Utilities;
|
||||||
|
using Address = Stripe.Address;
|
||||||
|
using Customer = Stripe.Customer;
|
||||||
|
using PaymentMethod = Stripe.PaymentMethod;
|
||||||
|
using Subscription = Stripe.Subscription;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Test.Billing;
|
namespace Bit.Commercial.Core.Test.Billing;
|
||||||
|
|
||||||
@ -833,7 +843,7 @@ public class ProviderBillingServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task SetupCustomer_Success(
|
public async Task SetupCustomer_NoPaymentMethod_Success(
|
||||||
SutProvider<ProviderBillingService> sutProvider,
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
Provider provider,
|
Provider provider,
|
||||||
TaxInfo taxInfo)
|
TaxInfo taxInfo)
|
||||||
@ -877,6 +887,301 @@ public class ProviderBillingServiceTests
|
|||||||
Assert.Equivalent(expected, actual);
|
Assert.Equivalent(expected, actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SetupCustomer_InvalidRequiredPaymentMethod_ThrowsBillingException(
|
||||||
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
|
Provider provider,
|
||||||
|
TaxInfo taxInfo,
|
||||||
|
TokenizedPaymentSource tokenizedPaymentSource)
|
||||||
|
{
|
||||||
|
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";
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||||
|
|
||||||
|
tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay };
|
||||||
|
|
||||||
|
await ThrowsBillingExceptionAsync(() =>
|
||||||
|
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SetupCustomer_WithBankAccount_Error_Reverts(
|
||||||
|
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 tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token");
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||||
|
|
||||||
|
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
|
||||||
|
options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([
|
||||||
|
new SetupIntent { Id = "setup_intent_id" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
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.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))
|
||||||
|
.Throws<StripeException>();
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns("setup_intent_id");
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<StripeException>(() =>
|
||||||
|
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_id");
|
||||||
|
|
||||||
|
await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_id", Arg.Is<SetupIntentCancelOptions>(options =>
|
||||||
|
options.CancellationReason == "abandoned"));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Remove(provider.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SetupCustomer_WithPayPal_Error_Reverts(
|
||||||
|
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 tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token");
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token)
|
||||||
|
.Returns("braintree_customer_id");
|
||||||
|
|
||||||
|
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.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
|
||||||
|
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
|
||||||
|
o.Metadata["region"] == "" &&
|
||||||
|
o.Metadata["btCustomerId"] == "braintree_customer_id" &&
|
||||||
|
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
|
||||||
|
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
|
||||||
|
.Throws<StripeException>();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<StripeException>(() =>
|
||||||
|
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IBraintreeGateway>().Customer.Received(1).DeleteAsync("braintree_customer_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SetupCustomer_WithBankAccount_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.BankAccount, "token");
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||||
|
|
||||||
|
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
|
||||||
|
options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([
|
||||||
|
new SetupIntent { Id = "setup_intent_id" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
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.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))
|
||||||
|
.Returns(expected);
|
||||||
|
|
||||||
|
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
|
||||||
|
|
||||||
|
Assert.Equivalent(expected, actual);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SetupCustomer_WithPayPal_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.PayPal, "token");
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token)
|
||||||
|
.Returns("braintree_customer_id");
|
||||||
|
|
||||||
|
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.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
|
||||||
|
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
|
||||||
|
o.Metadata["region"] == "" &&
|
||||||
|
o.Metadata["btCustomerId"] == "braintree_customer_id" &&
|
||||||
|
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
|
||||||
|
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
|
||||||
|
.Returns(expected);
|
||||||
|
|
||||||
|
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
|
||||||
|
|
||||||
|
Assert.Equivalent(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SetupCustomer_WithCard_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);
|
||||||
|
|
||||||
|
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))
|
||||||
|
.Returns(expected);
|
||||||
|
|
||||||
|
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
|
||||||
|
|
||||||
|
Assert.Equivalent(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid(
|
public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid(
|
||||||
SutProvider<ProviderBillingService> sutProvider,
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
@ -1044,7 +1349,7 @@ public class ProviderBillingServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task SetupSubscription_Succeeds(
|
public async Task SetupSubscription_SendInvoice_Succeeds(
|
||||||
SutProvider<ProviderBillingService> sutProvider,
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
Provider provider)
|
Provider provider)
|
||||||
{
|
{
|
||||||
@ -1127,6 +1432,303 @@ public class ProviderBillingServiceTests
|
|||||||
Assert.Equivalent(expected, actual);
|
Assert.Equivalent(expected, actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SetupSubscription_ChargeAutomatically_HasCard_Succeeds(
|
||||||
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
|
Provider provider)
|
||||||
|
{
|
||||||
|
provider.Type = ProviderType.Msp;
|
||||||
|
provider.GatewaySubscriptionId = null;
|
||||||
|
|
||||||
|
var customer = new Customer
|
||||||
|
{
|
||||||
|
Id = "customer_id",
|
||||||
|
InvoiceSettings = new CustomerInvoiceSettings
|
||||||
|
{
|
||||||
|
DefaultPaymentMethodId = "pm_123"
|
||||||
|
},
|
||||||
|
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||||
|
};
|
||||||
|
|
||||||
|
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<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);
|
||||||
|
|
||||||
|
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_ChargeAutomatically_HasBankAccount_Succeeds(
|
||||||
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
|
Provider provider)
|
||||||
|
{
|
||||||
|
provider.Type = ProviderType.Msp;
|
||||||
|
provider.GatewaySubscriptionId = null;
|
||||||
|
|
||||||
|
var customer = new Customer
|
||||||
|
{
|
||||||
|
Id = "customer_id",
|
||||||
|
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||||
|
Metadata = new Dictionary<string, string>(),
|
||||||
|
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||||
|
};
|
||||||
|
|
||||||
|
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<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);
|
||||||
|
|
||||||
|
const string setupIntentId = "seti_123";
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns(setupIntentId);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntentId, Arg.Is<SetupIntentGetOptions>(options =>
|
||||||
|
options.Expand.Contains("payment_method"))).Returns(new SetupIntent
|
||||||
|
{
|
||||||
|
Id = setupIntentId,
|
||||||
|
Status = "requires_action",
|
||||||
|
NextAction = new SetupIntentNextAction
|
||||||
|
{
|
||||||
|
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
|
||||||
|
},
|
||||||
|
PaymentMethod = new PaymentMethod
|
||||||
|
{
|
||||||
|
UsBankAccount = new PaymentMethodUsBankAccount()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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_ChargeAutomatically_HasPayPal_Succeeds(
|
||||||
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
|
Provider provider)
|
||||||
|
{
|
||||||
|
provider.Type = ProviderType.Msp;
|
||||||
|
provider.GatewaySubscriptionId = null;
|
||||||
|
|
||||||
|
var customer = new Customer
|
||||||
|
{
|
||||||
|
Id = "customer_id",
|
||||||
|
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||||
|
Metadata = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["btCustomerId"] = "braintree_customer_id"
|
||||||
|
},
|
||||||
|
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||||
|
};
|
||||||
|
|
||||||
|
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<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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region UpdateSeatMinimums
|
#region UpdateSeatMinimums
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
@ -15,11 +18,26 @@ namespace Bit.Commercial.Core.Test.SecretsManager.Queries.Projects;
|
|||||||
[SutProviderCustomize]
|
[SutProviderCustomize]
|
||||||
public class MaxProjectsQueryTests
|
public class MaxProjectsQueryTests
|
||||||
{
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task GetByOrgIdAsync_SelfHosted_ReturnsNulls(SutProvider<MaxProjectsQuery> sutProvider,
|
||||||
|
Guid organizationId)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
|
||||||
|
|
||||||
|
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1);
|
||||||
|
|
||||||
|
Assert.Null(max);
|
||||||
|
Assert.Null(overMax);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task GetByOrgIdAsync_OrganizationIsNull_ThrowsNotFound(SutProvider<MaxProjectsQuery> sutProvider,
|
public async Task GetByOrgIdAsync_OrganizationIsNull_ThrowsNotFound(SutProvider<MaxProjectsQuery> sutProvider,
|
||||||
Guid organizationId)
|
Guid organizationId)
|
||||||
{
|
{
|
||||||
|
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(default).ReturnsNull();
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(default).ReturnsNull();
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1));
|
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1));
|
||||||
@ -28,26 +46,6 @@ public class MaxProjectsQueryTests
|
|||||||
.GetProjectCountByOrganizationIdAsync(organizationId);
|
.GetProjectCountByOrganizationIdAsync(organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData(PlanType.FamiliesAnnually2019)]
|
|
||||||
[BitAutoData(PlanType.Custom)]
|
|
||||||
[BitAutoData(PlanType.FamiliesAnnually)]
|
|
||||||
public async Task GetByOrgIdAsync_SmPlanIsNull_ThrowsBadRequest(PlanType planType,
|
|
||||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
|
||||||
{
|
|
||||||
organization.PlanType = planType;
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>()
|
|
||||||
.GetByIdAsync(organization.Id)
|
|
||||||
.Returns(organization);
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1));
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IProjectRepository>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.GetProjectCountByOrganizationIdAsync(organization.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData(PlanType.TeamsMonthly2019)]
|
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||||
[BitAutoData(PlanType.TeamsMonthly2020)]
|
[BitAutoData(PlanType.TeamsMonthly2020)]
|
||||||
@ -65,9 +63,14 @@ public class MaxProjectsQueryTests
|
|||||||
public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType,
|
public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType,
|
||||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
||||||
{
|
{
|
||||||
|
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||||
|
|
||||||
organization.PlanType = planType;
|
organization.PlanType = planType;
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
|
||||||
|
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||||
|
|
||||||
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);
|
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);
|
||||||
|
|
||||||
Assert.Null(limit);
|
Assert.Null(limit);
|
||||||
@ -110,6 +113,9 @@ public class MaxProjectsQueryTests
|
|||||||
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
|
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
|
||||||
.Returns(projects);
|
.Returns(projects);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
|
||||||
|
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||||
|
|
||||||
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);
|
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);
|
||||||
|
|
||||||
Assert.NotNull(max);
|
Assert.NotNull(max);
|
||||||
|
@ -124,8 +124,20 @@ services:
|
|||||||
profiles:
|
profiles:
|
||||||
- servicebus
|
- servicebus
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
container_name: bw-redis
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
command: redis-server --appendonly yes
|
||||||
|
profiles:
|
||||||
|
- redis
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mssql_dev_data:
|
mssql_dev_data:
|
||||||
postgres_dev_data:
|
postgres_dev_data:
|
||||||
mysql_dev_data:
|
mysql_dev_data:
|
||||||
rabbitmq_data:
|
rabbitmq_data:
|
||||||
|
redis_data:
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
"rollForward": "latestFeature"
|
"rollForward": "latestFeature"
|
||||||
},
|
},
|
||||||
"msbuild-sdks": {
|
"msbuild-sdks": {
|
||||||
"Microsoft.Build.Traversal": "4.1.0"
|
"Microsoft.Build.Traversal": "4.1.0",
|
||||||
|
"Microsoft.Build.Sql": "0.1.9-preview"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ using Bit.Admin.Enums;
|
|||||||
using Bit.Admin.Models;
|
using Bit.Admin.Models;
|
||||||
using Bit.Admin.Services;
|
using Bit.Admin.Services;
|
||||||
using Bit.Admin.Utilities;
|
using Bit.Admin.Utilities;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -89,7 +88,7 @@ public class UsersController : Controller
|
|||||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
|
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
|
||||||
|
|
||||||
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
|
var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id);
|
||||||
return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain));
|
return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +105,7 @@ public class UsersController : Controller
|
|||||||
var billingInfo = await _paymentService.GetBillingAsync(user);
|
var billingInfo = await _paymentService.GetBillingAsync(user);
|
||||||
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
|
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
|
||||||
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
|
var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id);
|
||||||
var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id);
|
var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id);
|
||||||
|
|
||||||
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired));
|
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired));
|
||||||
@ -167,7 +166,6 @@ public class UsersController : Controller
|
|||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
[RequirePermission(Permission.User_NewDeviceException_Edit)]
|
[RequirePermission(Permission.User_NewDeviceException_Edit)]
|
||||||
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
|
|
||||||
public async Task<IActionResult> ToggleNewDeviceVerification(Guid id)
|
public async Task<IActionResult> ToggleNewDeviceVerification(Guid id)
|
||||||
{
|
{
|
||||||
var user = await _userRepository.GetByIdAsync(id);
|
var user = await _userRepository.GetByIdAsync(id);
|
||||||
@ -179,12 +177,4 @@ public class UsersController : Controller
|
|||||||
await _userService.ToggleNewDeviceVerificationException(user.Id);
|
await _userService.ToggleNewDeviceVerificationException(user.Id);
|
||||||
return RedirectToAction("Edit", new { id });
|
return RedirectToAction("Edit", new { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Feature flag to be removed in PM-14207
|
|
||||||
private async Task<bool?> AccountDeprovisioningEnabled(Guid userId)
|
|
||||||
{
|
|
||||||
return _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
|
||||||
? await _userService.IsClaimedByAnyOrganizationAsync(userId)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ namespace Bit.Api.AdminConsole.Authorization;
|
|||||||
public static class HttpContextExtensions
|
public static class HttpContextExtensions
|
||||||
{
|
{
|
||||||
public const string NoOrgIdError =
|
public const string NoOrgIdError =
|
||||||
"A route decorated with with '[Authorize<Requirement>]' must include a route value named 'orgId' either through the [Controller] attribute or through a '[Http*]' attribute.";
|
"A route decorated with with '[Authorize<Requirement>]' must include a route value named 'orgId' or 'organizationId' either through the [Controller] attribute or through a '[Http*]' attribute.";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the result of the callback, caching it in HttpContext.Features for the lifetime of the request.
|
/// Returns the result of the callback, caching it in HttpContext.Features for the lifetime of the request.
|
||||||
@ -61,19 +61,27 @@ public static class HttpContextExtensions
|
|||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parses the {orgId} route parameter into a Guid, or throws if the {orgId} is not present or not a valid guid.
|
/// Parses the {orgId} or {organizationId} route parameter into a Guid, or throws if neither are present or are not valid guids.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="httpContext"></param>
|
/// <param name="httpContext"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
/// <exception cref="InvalidOperationException"></exception>
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
public static Guid GetOrganizationId(this HttpContext httpContext)
|
public static Guid GetOrganizationId(this HttpContext httpContext)
|
||||||
{
|
{
|
||||||
httpContext.GetRouteData().Values.TryGetValue("orgId", out var orgIdParam);
|
var routeValues = httpContext.GetRouteData().Values;
|
||||||
if (orgIdParam == null || !Guid.TryParse(orgIdParam.ToString(), out var orgId))
|
|
||||||
|
routeValues.TryGetValue("orgId", out var orgIdParam);
|
||||||
|
if (orgIdParam != null && Guid.TryParse(orgIdParam.ToString(), out var orgId))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(NoOrgIdError);
|
return orgId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return orgId;
|
routeValues.TryGetValue("organizationId", out var organizationIdParam);
|
||||||
|
if (organizationIdParam != null && Guid.TryParse(organizationIdParam.ToString(), out var organizationId))
|
||||||
|
{
|
||||||
|
return organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException(NoOrgIdError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
};
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Authorization.Requirements;
|
||||||
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
using Bit.Api.Models.Request.Organizations;
|
using Bit.Api.Models.Request.Organizations;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
@ -63,6 +64,7 @@ public class OrganizationUsersController : Controller
|
|||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
|
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
|
||||||
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||||
|
private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand;
|
||||||
|
|
||||||
public OrganizationUsersController(
|
public OrganizationUsersController(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@ -89,7 +91,8 @@ public class OrganizationUsersController : Controller
|
|||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IPricingClient pricingClient,
|
IPricingClient pricingClient,
|
||||||
IConfirmOrganizationUserCommand confirmOrganizationUserCommand,
|
IConfirmOrganizationUserCommand confirmOrganizationUserCommand,
|
||||||
IRestoreOrganizationUserCommand restoreOrganizationUserCommand)
|
IRestoreOrganizationUserCommand restoreOrganizationUserCommand,
|
||||||
|
IInitPendingOrganizationCommand initPendingOrganizationCommand)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@ -116,6 +119,7 @@ public class OrganizationUsersController : Controller
|
|||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
_confirmOrganizationUserCommand = confirmOrganizationUserCommand;
|
_confirmOrganizationUserCommand = confirmOrganizationUserCommand;
|
||||||
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||||
|
_initPendingOrganizationCommand = initPendingOrganizationCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
@ -159,6 +163,12 @@ public class OrganizationUsersController : Controller
|
|||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false)
|
public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.SeparateCustomRolePermissions))
|
||||||
|
{
|
||||||
|
return await GetvNextAsync(orgId, includeGroups, includeCollections);
|
||||||
|
}
|
||||||
|
|
||||||
var authorized = (await _authorizationService.AuthorizeAsync(
|
var authorized = (await _authorizationService.AuthorizeAsync(
|
||||||
User, new OrganizationScope(orgId), OrganizationUserUserDetailsOperations.ReadAll)).Succeeded;
|
User, new OrganizationScope(orgId), OrganizationUserUserDetailsOperations.ReadAll)).Succeeded;
|
||||||
if (!authorized)
|
if (!authorized)
|
||||||
@ -188,6 +198,37 @@ public class OrganizationUsersController : Controller
|
|||||||
return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(responses);
|
return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> GetvNextAsync(Guid orgId, bool includeGroups = false, bool includeCollections = false)
|
||||||
|
{
|
||||||
|
var request = new OrganizationUserUserDetailsQueryRequest
|
||||||
|
{
|
||||||
|
OrganizationId = orgId,
|
||||||
|
IncludeGroups = includeGroups,
|
||||||
|
IncludeCollections = includeCollections,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ((await _authorizationService.AuthorizeAsync(User, new ManageUsersRequirement())).Succeeded)
|
||||||
|
{
|
||||||
|
return GetResultListResponseModel(await _organizationUserUserDetailsQuery.Get(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((await _authorizationService.AuthorizeAsync(User, new ManageAccountRecoveryRequirement())).Succeeded)
|
||||||
|
{
|
||||||
|
return GetResultListResponseModel(await _organizationUserUserDetailsQuery.GetAccountRecoveryEnrolledUsers(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListResponseModel<OrganizationUserUserDetailsResponseModel> GetResultListResponseModel(IEnumerable<(OrganizationUserUserDetails OrgUser,
|
||||||
|
bool TwoFactorEnabled, bool ClaimedByOrganization)> results)
|
||||||
|
{
|
||||||
|
return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(results
|
||||||
|
.Select(result => new OrganizationUserUserDetailsResponseModel(result))
|
||||||
|
.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpGet("{id}/groups")]
|
[HttpGet("{id}/groups")]
|
||||||
public async Task<IEnumerable<string>> GetGroups(string orgId, string id)
|
public async Task<IEnumerable<string>> GetGroups(string orgId, string id)
|
||||||
{
|
{
|
||||||
@ -313,7 +354,7 @@ public class OrganizationUsersController : Controller
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _organizationService.InitPendingOrganization(user, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName, model.Token);
|
await _initPendingOrganizationCommand.InitPendingOrganizationAsync(user, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName, model.Token);
|
||||||
await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService);
|
await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService);
|
||||||
await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id);
|
await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id);
|
||||||
}
|
}
|
||||||
@ -575,7 +616,6 @@ public class OrganizationUsersController : Controller
|
|||||||
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
|
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
|
||||||
[HttpDelete("{id}/delete-account")]
|
[HttpDelete("{id}/delete-account")]
|
||||||
[HttpPost("{id}/delete-account")]
|
[HttpPost("{id}/delete-account")]
|
||||||
public async Task DeleteAccount(Guid orgId, Guid id)
|
public async Task DeleteAccount(Guid orgId, Guid id)
|
||||||
@ -594,7 +634,6 @@ public class OrganizationUsersController : Controller
|
|||||||
await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
|
await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
|
||||||
[HttpDelete("delete-account")]
|
[HttpDelete("delete-account")]
|
||||||
[HttpPost("delete-account")]
|
[HttpPost("delete-account")]
|
||||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||||
@ -719,11 +758,6 @@ public class OrganizationUsersController : Controller
|
|||||||
|
|
||||||
private async Task<IDictionary<Guid, bool>> GetClaimedByOrganizationStatusAsync(Guid orgId, IEnumerable<Guid> userIds)
|
private async Task<IDictionary<Guid, bool>> GetClaimedByOrganizationStatusAsync(Guid orgId, IEnumerable<Guid> userIds)
|
||||||
{
|
{
|
||||||
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
|
||||||
{
|
|
||||||
return userIds.ToDictionary(kvp => kvp, kvp => false);
|
|
||||||
}
|
|
||||||
|
|
||||||
var usersOrganizationClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgId, userIds);
|
var usersOrganizationClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgId, userIds);
|
||||||
return usersOrganizationClaimedStatus;
|
return usersOrganizationClaimedStatus;
|
||||||
}
|
}
|
||||||
|
@ -279,8 +279,7 @@ public class OrganizationsController : Controller
|
|||||||
throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving.");
|
throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
if ((await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id))
|
||||||
&& (await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id))
|
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Claimed user account cannot leave claiming organization. Contact your organization administrator for additional details.");
|
throw new BadRequestException("Claimed user account cannot leave claiming organization. Contact your organization administrator for additional details.");
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
using Bit.Api.AdminConsole.Models.Response.Helpers;
|
using Bit.Api.AdminConsole.Models.Response.Helpers;
|
||||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||||
@ -79,7 +78,7 @@ public class PoliciesController : Controller
|
|||||||
return new PolicyDetailResponseModel(new Policy { Type = (PolicyType)type });
|
return new PolicyDetailResponseModel(new Policy { Type = (PolicyType)type });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && policy.Type is PolicyType.SingleOrg)
|
if (policy.Type is PolicyType.SingleOrg)
|
||||||
{
|
{
|
||||||
return await policy.GetSingleOrgPolicyDetailResponseAsync(_organizationHasVerifiedDomainsQuery);
|
return await policy.GetSingleOrgPolicyDetailResponseAsync(_organizationHasVerifiedDomainsQuery);
|
||||||
}
|
}
|
||||||
|
@ -84,22 +84,22 @@ public class ProvidersController : Controller
|
|||||||
|
|
||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
|
|
||||||
var taxInfo = model.TaxInfo != null
|
var taxInfo = new TaxInfo
|
||||||
? new TaxInfo
|
{
|
||||||
{
|
BillingAddressCountry = model.TaxInfo.Country,
|
||||||
BillingAddressCountry = model.TaxInfo.Country,
|
BillingAddressPostalCode = model.TaxInfo.PostalCode,
|
||||||
BillingAddressPostalCode = model.TaxInfo.PostalCode,
|
TaxIdNumber = model.TaxInfo.TaxId,
|
||||||
TaxIdNumber = model.TaxInfo.TaxId,
|
BillingAddressLine1 = model.TaxInfo.Line1,
|
||||||
BillingAddressLine1 = model.TaxInfo.Line1,
|
BillingAddressLine2 = model.TaxInfo.Line2,
|
||||||
BillingAddressLine2 = model.TaxInfo.Line2,
|
BillingAddressCity = model.TaxInfo.City,
|
||||||
BillingAddressCity = model.TaxInfo.City,
|
BillingAddressState = model.TaxInfo.State
|
||||||
BillingAddressState = model.TaxInfo.State
|
};
|
||||||
}
|
|
||||||
: null;
|
var tokenizedPaymentSource = model.PaymentSource?.ToDomain();
|
||||||
|
|
||||||
var response =
|
var response =
|
||||||
await _providerService.CompleteSetupAsync(model.ToProvider(provider), userId, model.Token, model.Key,
|
await _providerService.CompleteSetupAsync(model.ToProvider(provider), userId, model.Token, model.Key,
|
||||||
taxInfo);
|
taxInfo, tokenizedPaymentSource);
|
||||||
|
|
||||||
return new ProviderResponseModel(response);
|
return new ProviderResponseModel(response);
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,8 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
|||||||
|
|
||||||
public string InitiationPath { get; set; }
|
public string InitiationPath { get; set; }
|
||||||
|
|
||||||
|
public bool SkipTrial { get; set; }
|
||||||
|
|
||||||
public virtual OrganizationSignup ToOrganizationSignup(User user)
|
public virtual OrganizationSignup ToOrganizationSignup(User user)
|
||||||
{
|
{
|
||||||
var orgSignup = new OrganizationSignup
|
var orgSignup = new OrganizationSignup
|
||||||
@ -107,6 +109,7 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
|||||||
BillingAddressCountry = BillingAddressCountry,
|
BillingAddressCountry = BillingAddressCountry,
|
||||||
},
|
},
|
||||||
InitiationPath = InitiationPath,
|
InitiationPath = InitiationPath,
|
||||||
|
SkipTrial = SkipTrial
|
||||||
};
|
};
|
||||||
|
|
||||||
Keys?.ToOrganizationSignup(orgSignup);
|
Keys?.ToOrganizationSignup(orgSignup);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Api.Billing.Models.Requests;
|
||||||
using Bit.Api.Models.Request;
|
using Bit.Api.Models.Request;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
@ -23,7 +24,9 @@ public class ProviderSetupRequestModel
|
|||||||
public string Token { get; set; }
|
public string Token { get; set; }
|
||||||
[Required]
|
[Required]
|
||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
|
[Required]
|
||||||
public ExpandedTaxInfoUpdateRequestModel TaxInfo { get; set; }
|
public ExpandedTaxInfoUpdateRequestModel TaxInfo { get; set; }
|
||||||
|
public TokenizedPaymentSourceRequestBody PaymentSource { get; set; }
|
||||||
|
|
||||||
public virtual Provider ToProvider(Provider provider)
|
public virtual Provider ToProvider(Provider provider)
|
||||||
{
|
{
|
||||||
|
@ -126,6 +126,26 @@ public class OrganizationUserUserMiniDetailsResponseModel : ResponseModel
|
|||||||
|
|
||||||
public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel
|
public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel
|
||||||
{
|
{
|
||||||
|
public OrganizationUserUserDetailsResponseModel((OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization) data, string obj = "organizationUserUserDetails")
|
||||||
|
: base(data.OrgUser, obj)
|
||||||
|
{
|
||||||
|
if (data.OrgUser == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(data.OrgUser));
|
||||||
|
}
|
||||||
|
|
||||||
|
Name = data.OrgUser.Name;
|
||||||
|
Email = data.OrgUser.Email;
|
||||||
|
AvatarColor = data.OrgUser.AvatarColor;
|
||||||
|
TwoFactorEnabled = data.TwoFactorEnabled;
|
||||||
|
SsoBound = !string.IsNullOrWhiteSpace(data.OrgUser.SsoExternalId);
|
||||||
|
Collections = data.OrgUser.Collections.Select(c => new SelectionReadOnlyResponseModel(c));
|
||||||
|
Groups = data.OrgUser.Groups;
|
||||||
|
// Prevent reset password when using key connector.
|
||||||
|
ResetPasswordEnrolled = ResetPasswordEnrolled && !data.OrgUser.UsesKeyConnector;
|
||||||
|
ClaimedByOrganization = data.ClaimedByOrganization;
|
||||||
|
}
|
||||||
|
|
||||||
public OrganizationUserUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,
|
public OrganizationUserUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,
|
||||||
bool twoFactorEnabled, bool claimedByOrganization, string obj = "organizationUserUserDetails")
|
bool twoFactorEnabled, bool claimedByOrganization, string obj = "organizationUserUserDetails")
|
||||||
: base(organizationUser, obj)
|
: base(organizationUser, obj)
|
||||||
|
@ -58,7 +58,8 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
ProviderName = organization.ProviderName;
|
ProviderName = organization.ProviderName;
|
||||||
ProviderType = organization.ProviderType;
|
ProviderType = organization.ProviderType;
|
||||||
FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName;
|
FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName;
|
||||||
FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null &&
|
IsAdminInitiated = organization.IsAdminInitiated ?? false;
|
||||||
|
FamilySponsorshipAvailable = (FamilySponsorshipFriendlyName == null || IsAdminInitiated) &&
|
||||||
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
|
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
|
||||||
.UsersCanSponsor(organization);
|
.UsersCanSponsor(organization);
|
||||||
ProductTierType = organization.PlanType.GetProductTier();
|
ProductTierType = organization.PlanType.GetProductTier();
|
||||||
@ -135,7 +136,6 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Obsolete.
|
/// Obsolete.
|
||||||
///
|
|
||||||
/// See <see cref="UserIsClaimedByOrganization"/>
|
/// See <see cref="UserIsClaimedByOrganization"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Please use UserIsClaimedByOrganization instead. This property will be removed in a future version.")]
|
[Obsolete("Please use UserIsClaimedByOrganization instead. This property will be removed in a future version.")]
|
||||||
@ -145,16 +145,14 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
set => UserIsClaimedByOrganization = value;
|
set => UserIsClaimedByOrganization = value;
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates if the organization claims the user.
|
/// Indicates if the user is claimed by the organization.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// An organization claims a user if the user's email domain is verified by the organization and the user is a member of it.
|
/// A user is claimed by an organization if the user's email domain is verified by the organization and the user is a member.
|
||||||
/// The organization must be enabled and able to have verified domains.
|
/// The organization must be enabled and able to have verified domains.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <returns>
|
|
||||||
/// False if the Account Deprovisioning feature flag is disabled.
|
|
||||||
/// </returns>
|
|
||||||
public bool UserIsClaimedByOrganization { get; set; }
|
public bool UserIsClaimedByOrganization { get; set; }
|
||||||
public bool UseRiskInsights { get; set; }
|
public bool UseRiskInsights { get; set; }
|
||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
|
public bool IsAdminInitiated { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ public class MembersController : Controller
|
|||||||
{
|
{
|
||||||
return new NotFoundResult();
|
return new NotFoundResult();
|
||||||
}
|
}
|
||||||
var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser),
|
var response = new MemberResponseModel(orgUser, await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUser),
|
||||||
collections);
|
collections);
|
||||||
return new JsonResult(response);
|
return new JsonResult(response);
|
||||||
}
|
}
|
||||||
@ -185,7 +185,7 @@ public class MembersController : Controller
|
|||||||
{
|
{
|
||||||
var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
||||||
response = new MemberResponseModel(existingUserDetails,
|
response = new MemberResponseModel(existingUserDetails,
|
||||||
await _userService.TwoFactorIsEnabledAsync(existingUserDetails), associations);
|
await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(existingUserDetails), associations);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -4,8 +4,6 @@
|
|||||||
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||||
<ANCMPreConfiguredForIIS>true</ANCMPreConfiguredForIIS>
|
<ANCMPreConfiguredForIIS>true</ANCMPreConfiguredForIIS>
|
||||||
<!-- Temp exclusions until warnings are fixed -->
|
|
||||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8604</WarningsNotAsErrors>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
@ -16,6 +16,7 @@ using Bit.Core.Auth.Entities;
|
|||||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -45,6 +46,7 @@ public class AccountsController : Controller
|
|||||||
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
|
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
|
||||||
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
||||||
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
|
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
|
||||||
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
|
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
|
||||||
@ -68,6 +70,7 @@ public class AccountsController : Controller
|
|||||||
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
|
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
|
||||||
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
|
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
|
||||||
IRotateUserKeyCommand rotateUserKeyCommand,
|
IRotateUserKeyCommand rotateUserKeyCommand,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
|
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
|
||||||
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
|
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
|
||||||
@ -87,6 +90,7 @@ public class AccountsController : Controller
|
|||||||
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
|
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
|
||||||
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
|
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
|
||||||
_rotateUserKeyCommand = rotateUserKeyCommand;
|
_rotateUserKeyCommand = rotateUserKeyCommand;
|
||||||
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_cipherValidator = cipherValidator;
|
_cipherValidator = cipherValidator;
|
||||||
_folderValidator = folderValidator;
|
_folderValidator = folderValidator;
|
||||||
@ -389,7 +393,7 @@ public class AccountsController : Controller
|
|||||||
await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,
|
await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,
|
||||||
ProviderUserStatusType.Confirmed);
|
ProviderUserStatusType.Confirmed);
|
||||||
|
|
||||||
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
|
|
||||||
@ -423,7 +427,7 @@ public class AccountsController : Controller
|
|||||||
|
|
||||||
await _userService.SaveUserAsync(model.ToUser(user));
|
await _userService.SaveUserAsync(model.ToUser(user));
|
||||||
|
|
||||||
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
|
|
||||||
@ -442,7 +446,7 @@ public class AccountsController : Controller
|
|||||||
}
|
}
|
||||||
await _userService.SaveUserAsync(model.ToUser(user), true);
|
await _userService.SaveUserAsync(model.ToUser(user), true);
|
||||||
|
|
||||||
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
|
|
||||||
@ -514,9 +518,8 @@ public class AccountsController : Controller
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization.
|
// Check if the user is claimed by any organization.
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||||
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.");
|
throw new BadRequestException("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.");
|
||||||
}
|
}
|
||||||
@ -693,7 +696,6 @@ public class AccountsController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpPost("resend-new-device-otp")]
|
[HttpPost("resend-new-device-otp")]
|
||||||
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)
|
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using Bit.Api.Billing.Models.Responses;
|
using Bit.Api.Billing.Models.Responses;
|
||||||
using Bit.Core.Billing.Models.Api.Requests.Accounts;
|
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Tax.Requests;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
@ -3,6 +3,7 @@ using Bit.Api.Models.Request;
|
|||||||
using Bit.Api.Models.Request.Accounts;
|
using Bit.Api.Models.Request.Accounts;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Api.Utilities;
|
using Bit.Api.Utilities;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -22,7 +23,8 @@ namespace Bit.Api.Billing.Controllers;
|
|||||||
[Route("accounts")]
|
[Route("accounts")]
|
||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
public class AccountsController(
|
public class AccountsController(
|
||||||
IUserService userService) : Controller
|
IUserService userService,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller
|
||||||
{
|
{
|
||||||
[HttpPost("premium")]
|
[HttpPost("premium")]
|
||||||
public async Task<PaymentResponseModel> PostPremiumAsync(
|
public async Task<PaymentResponseModel> PostPremiumAsync(
|
||||||
@ -56,7 +58,7 @@ public class AccountsController(
|
|||||||
model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license,
|
model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license,
|
||||||
new TaxInfo { BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode });
|
new TaxInfo { BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode });
|
||||||
|
|
||||||
var userTwoFactorEnabled = await userService.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
|
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Billing.Models.Api.Requests.Organizations;
|
using Bit.Core.Billing.Tax.Requests;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
using System.Diagnostics;
|
||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
using Bit.Api.Billing.Models.Requests;
|
using Bit.Api.Billing.Models.Requests;
|
||||||
using Bit.Api.Billing.Models.Responses;
|
using Bit.Api.Billing.Models.Responses;
|
||||||
|
using Bit.Api.Billing.Queries.Organizations;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Models.Sales;
|
using Bit.Core.Billing.Models.Sales;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Tax.Models;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -24,6 +27,7 @@ public class OrganizationBillingController(
|
|||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IOrganizationBillingService organizationBillingService,
|
IOrganizationBillingService organizationBillingService,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationWarningsQuery organizationWarningsQuery,
|
||||||
IPaymentService paymentService,
|
IPaymentService paymentService,
|
||||||
IPricingClient pricingClient,
|
IPricingClient pricingClient,
|
||||||
ISubscriberService subscriberService,
|
ISubscriberService subscriberService,
|
||||||
@ -290,6 +294,7 @@ public class OrganizationBillingController(
|
|||||||
sale.SubscriptionSetup.SkipTrial = true;
|
sale.SubscriptionSetup.SkipTrial = true;
|
||||||
await organizationBillingService.Finalize(sale);
|
await organizationBillingService.Finalize(sale);
|
||||||
var org = await organizationRepository.GetByIdAsync(organizationId);
|
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.");
|
||||||
if (organizationSignup.PaymentMethodType != null)
|
if (organizationSignup.PaymentMethodType != null)
|
||||||
{
|
{
|
||||||
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);
|
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);
|
||||||
@ -335,4 +340,28 @@ public class OrganizationBillingController(
|
|||||||
|
|
||||||
return TypedResults.Ok(providerId);
|
return TypedResults.Ok(providerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("warnings")]
|
||||||
|
public async Task<IResult> GetWarningsAsync([FromRoute] Guid organizationId)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* We'll keep these available at the User level, because we're hiding any pertinent information and
|
||||||
|
* we want to throw as few errors as possible since these are not core features.
|
||||||
|
*/
|
||||||
|
if (!await currentContext.OrganizationUser(organizationId))
|
||||||
|
{
|
||||||
|
return Error.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
return Error.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await organizationWarningsQuery.Run(organization);
|
||||||
|
|
||||||
|
return TypedResults.Ok(response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Api.Models.Request.Organizations;
|
using Bit.Api.Models.Request.Organizations;
|
||||||
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Api.Models.Response.Organizations;
|
using Bit.Api.Models.Response.Organizations;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
|
||||||
@ -8,6 +9,7 @@ using Bit.Core.Entities;
|
|||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Api.Request.OrganizationSponsorships;
|
using Bit.Core.Models.Api.Request.OrganizationSponsorships;
|
||||||
using Bit.Core.Models.Api.Response.OrganizationSponsorships;
|
using Bit.Core.Models.Api.Response.OrganizationSponsorships;
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationSponsorships;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -105,13 +107,16 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
model.FriendlyName,
|
model.FriendlyName,
|
||||||
model.IsAdminInitiated.GetValueOrDefault(),
|
model.IsAdminInitiated.GetValueOrDefault(),
|
||||||
model.Notes);
|
model.Notes);
|
||||||
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name);
|
if (sponsorship.OfferedToEmail != null)
|
||||||
|
{
|
||||||
|
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
[HttpPost("{sponsoringOrgId}/families-for-enterprise/resend")]
|
[HttpPost("{sponsoringOrgId}/families-for-enterprise/resend")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task ResendSponsorshipOffer(Guid sponsoringOrgId)
|
public async Task ResendSponsorshipOffer(Guid sponsoringOrgId, [FromQuery] string sponsoredFriendlyName)
|
||||||
{
|
{
|
||||||
var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsoringOrgId,
|
var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsoringOrgId,
|
||||||
PolicyType.FreeFamiliesSponsorshipPolicy);
|
PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||||
@ -124,11 +129,14 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
var sponsoringOrgUser = await _organizationUserRepository
|
var sponsoringOrgUser = await _organizationUserRepository
|
||||||
.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default);
|
.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default);
|
||||||
|
|
||||||
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(
|
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);
|
||||||
await _organizationRepository.GetByIdAsync(sponsoringOrgId),
|
var filteredSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName != null && s.FriendlyName.Equals(sponsoredFriendlyName, StringComparison.OrdinalIgnoreCase));
|
||||||
sponsoringOrgUser,
|
if (filteredSponsorship != null)
|
||||||
await _organizationSponsorshipRepository
|
{
|
||||||
.GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id));
|
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(
|
||||||
|
await _organizationRepository.GetByIdAsync(sponsoringOrgId),
|
||||||
|
sponsoringOrgUser, filteredSponsorship);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
@ -199,7 +207,7 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
[HttpDelete("{sponsoringOrganizationId}")]
|
[HttpDelete("{sponsoringOrganizationId}")]
|
||||||
[HttpPost("{sponsoringOrganizationId}/delete")]
|
[HttpPost("{sponsoringOrganizationId}/delete")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task RevokeSponsorship(Guid sponsoringOrganizationId)
|
public async Task RevokeSponsorship(Guid sponsoringOrganizationId, [FromQuery] bool isAdminInitiated = false)
|
||||||
{
|
{
|
||||||
|
|
||||||
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrganizationId, _currentContext.UserId ?? default);
|
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrganizationId, _currentContext.UserId ?? default);
|
||||||
@ -209,7 +217,7 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var existingOrgSponsorship = await _organizationSponsorshipRepository
|
var existingOrgSponsorship = await _organizationSponsorshipRepository
|
||||||
.GetBySponsoringOrganizationUserIdAsync(orgUser.Id);
|
.GetBySponsoringOrganizationUserIdAsync(orgUser.Id, isAdminInitiated);
|
||||||
|
|
||||||
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
|
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
|
||||||
}
|
}
|
||||||
@ -246,5 +254,30 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
return new OrganizationSponsorshipSyncStatusResponseModel(lastSyncDate);
|
return new OrganizationSponsorshipSyncStatusResponseModel(lastSyncDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize("Application")]
|
||||||
|
[HttpGet("{sponsoringOrgId}/sponsored")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task<ListResponseModel<OrganizationSponsorshipInvitesResponseModel>> GetSponsoredOrganizations(Guid sponsoringOrgId)
|
||||||
|
{
|
||||||
|
var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgId);
|
||||||
|
if (sponsoringOrg == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
var organization = _currentContext.Organizations.First(x => x.Id == sponsoringOrg.Id);
|
||||||
|
if (!await _currentContext.OrganizationOwner(sponsoringOrg.Id) && !await _currentContext.OrganizationAdmin(sponsoringOrg.Id) && !organization.Permissions.ManageUsers)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);
|
||||||
|
return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(
|
||||||
|
sponsorships
|
||||||
|
.Where(s => s.IsAdminInitiated)
|
||||||
|
.Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private Task<User> CurrentUser => _userService.GetUserByIdAsync(_currentContext.UserId.Value);
|
private Task<User> CurrentUser => _userService.GetUserByIdAsync(_currentContext.UserId.Value);
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ using Bit.Core.Billing.Models;
|
|||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Tax.Models;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Models.BitStripe;
|
using Bit.Core.Models.BitStripe;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Tax.Services;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
|
36
src/Api/Billing/Controllers/TaxController.cs
Normal file
36
src/Api/Billing/Controllers/TaxController.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using Bit.Api.Billing.Models.Requests;
|
||||||
|
using Bit.Core.Billing.Tax.Commands;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bit.Api.Billing.Controllers;
|
||||||
|
|
||||||
|
[Authorize("Application")]
|
||||||
|
[Route("tax")]
|
||||||
|
public class TaxController(
|
||||||
|
IPreviewTaxAmountCommand previewTaxAmountCommand) : BaseBillingController
|
||||||
|
{
|
||||||
|
[HttpPost("preview-amount/organization-trial")]
|
||||||
|
public async Task<IResult> PreviewTaxAmountForOrganizationTrialAsync(
|
||||||
|
[FromBody] PreviewTaxAmountForOrganizationTrialRequestBody requestBody)
|
||||||
|
{
|
||||||
|
var parameters = new OrganizationTrialParameters
|
||||||
|
{
|
||||||
|
PlanType = requestBody.PlanType,
|
||||||
|
ProductType = requestBody.ProductType,
|
||||||
|
TaxInformation = new OrganizationTrialParameters.TaxInformationDTO
|
||||||
|
{
|
||||||
|
Country = requestBody.TaxInformation.Country,
|
||||||
|
PostalCode = requestBody.TaxInformation.PostalCode,
|
||||||
|
TaxId = requestBody.TaxInformation.TaxId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await previewTaxAmountCommand.Run(parameters);
|
||||||
|
|
||||||
|
return result.Match<IResult>(
|
||||||
|
taxAmount => TypedResults.Ok(new { TaxAmount = taxAmount }),
|
||||||
|
badRequest => Error.BadRequest(badRequest.TranslationKey),
|
||||||
|
unhandled => Error.ServerError(unhandled.TranslationKey));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Api.Billing.Models.Requests;
|
||||||
|
|
||||||
|
public class PreviewTaxAmountForOrganizationTrialRequestBody
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public PlanType PlanType { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public ProductType ProductType { get; set; }
|
||||||
|
|
||||||
|
[Required] public TaxInformationDTO TaxInformation { get; set; } = null!;
|
||||||
|
|
||||||
|
public class TaxInformationDTO
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Country { get; set; } = null!;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string PostalCode { get; set; } = null!;
|
||||||
|
|
||||||
|
public string? TaxId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Tax.Models;
|
||||||
|
|
||||||
namespace Bit.Api.Billing.Models.Requests;
|
namespace Bit.Api.Billing.Models.Requests;
|
||||||
|
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
#nullable enable
|
||||||
|
namespace Bit.Api.Billing.Models.Responses.Organizations;
|
||||||
|
|
||||||
|
public record OrganizationWarningsResponse
|
||||||
|
{
|
||||||
|
public FreeTrialWarning? FreeTrial { get; set; }
|
||||||
|
public InactiveSubscriptionWarning? InactiveSubscription { get; set; }
|
||||||
|
public ResellerRenewalWarning? ResellerRenewal { get; set; }
|
||||||
|
|
||||||
|
public record FreeTrialWarning
|
||||||
|
{
|
||||||
|
public int RemainingTrialDays { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record InactiveSubscriptionWarning
|
||||||
|
{
|
||||||
|
public required string Resolution { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ResellerRenewalWarning
|
||||||
|
{
|
||||||
|
public required string Type { get; set; }
|
||||||
|
public UpcomingRenewal? Upcoming { get; set; }
|
||||||
|
public IssuedRenewal? Issued { get; set; }
|
||||||
|
public PastDueRenewal? PastDue { get; set; }
|
||||||
|
|
||||||
|
public record UpcomingRenewal
|
||||||
|
{
|
||||||
|
public required DateTime RenewalDate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record IssuedRenewal
|
||||||
|
{
|
||||||
|
public required DateTime IssuedDate { get; set; }
|
||||||
|
public required DateTime DueDate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PastDueRenewal
|
||||||
|
{
|
||||||
|
public required DateTime SuspensionDate { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
|
using Bit.Core.Billing.Tax.Models;
|
||||||
|
|
||||||
namespace Bit.Api.Billing.Models.Responses;
|
namespace Bit.Api.Billing.Models.Responses;
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
|
using Bit.Core.Billing.Tax.Models;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Api.Billing.Models.Responses;
|
namespace Bit.Api.Billing.Models.Responses;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Tax.Models;
|
||||||
|
|
||||||
namespace Bit.Api.Billing.Models.Responses;
|
namespace Bit.Api.Billing.Models.Responses;
|
||||||
|
|
||||||
|
@ -0,0 +1,214 @@
|
|||||||
|
// ReSharper disable InconsistentNaming
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using Bit.Api.Billing.Models.Responses.Organizations;
|
||||||
|
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.Constants;
|
||||||
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Stripe;
|
||||||
|
using FreeTrialWarning = Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.FreeTrialWarning;
|
||||||
|
using InactiveSubscriptionWarning =
|
||||||
|
Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.InactiveSubscriptionWarning;
|
||||||
|
using ResellerRenewalWarning =
|
||||||
|
Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.ResellerRenewalWarning;
|
||||||
|
|
||||||
|
namespace Bit.Api.Billing.Queries.Organizations;
|
||||||
|
|
||||||
|
public interface IOrganizationWarningsQuery
|
||||||
|
{
|
||||||
|
Task<OrganizationWarningsResponse> Run(
|
||||||
|
Organization organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrganizationWarningsQuery(
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
IProviderRepository providerRepository,
|
||||||
|
IStripeAdapter stripeAdapter,
|
||||||
|
ISubscriberService subscriberService) : IOrganizationWarningsQuery
|
||||||
|
{
|
||||||
|
public async Task<OrganizationWarningsResponse> Run(
|
||||||
|
Organization organization)
|
||||||
|
{
|
||||||
|
var response = new OrganizationWarningsResponse();
|
||||||
|
|
||||||
|
var subscription =
|
||||||
|
await subscriberService.GetSubscription(organization,
|
||||||
|
new SubscriptionGetOptions { Expand = ["customer", "latest_invoice", "test_clock"] });
|
||||||
|
|
||||||
|
if (subscription == null)
|
||||||
|
{
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.FreeTrial = await GetFreeTrialWarning(organization, subscription);
|
||||||
|
|
||||||
|
var provider = await providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||||
|
|
||||||
|
response.InactiveSubscription = await GetInactiveSubscriptionWarning(organization, provider, subscription);
|
||||||
|
|
||||||
|
response.ResellerRenewal = await GetResellerRenewalWarning(provider, subscription);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<FreeTrialWarning?> GetFreeTrialWarning(
|
||||||
|
Organization organization,
|
||||||
|
Subscription subscription)
|
||||||
|
{
|
||||||
|
if (!await currentContext.EditSubscription(organization.Id))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscription is not
|
||||||
|
{
|
||||||
|
Status: StripeConstants.SubscriptionStatus.Trialing,
|
||||||
|
TrialEnd: not null,
|
||||||
|
Customer: not null
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var customer = subscription.Customer;
|
||||||
|
|
||||||
|
var hasPaymentMethod =
|
||||||
|
!string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) ||
|
||||||
|
!string.IsNullOrEmpty(customer.DefaultSourceId) ||
|
||||||
|
customer.Metadata.ContainsKey(StripeConstants.MetadataKeys.BraintreeCustomerId);
|
||||||
|
|
||||||
|
if (hasPaymentMethod)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow;
|
||||||
|
|
||||||
|
var remainingTrialDays = (int)Math.Ceiling((subscription.TrialEnd.Value - now).TotalDays);
|
||||||
|
|
||||||
|
return new FreeTrialWarning { RemainingTrialDays = remainingTrialDays };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<InactiveSubscriptionWarning?> GetInactiveSubscriptionWarning(
|
||||||
|
Organization organization,
|
||||||
|
Provider? provider,
|
||||||
|
Subscription subscription)
|
||||||
|
{
|
||||||
|
if (organization.Enabled ||
|
||||||
|
subscription.Status is not StripeConstants.SubscriptionStatus.Unpaid
|
||||||
|
and not StripeConstants.SubscriptionStatus.Canceled)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider != null)
|
||||||
|
{
|
||||||
|
return new InactiveSubscriptionWarning { Resolution = "contact_provider" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await currentContext.OrganizationOwner(organization.Id))
|
||||||
|
{
|
||||||
|
return subscription.Status switch
|
||||||
|
{
|
||||||
|
StripeConstants.SubscriptionStatus.Unpaid => new InactiveSubscriptionWarning
|
||||||
|
{
|
||||||
|
Resolution = "add_payment_method"
|
||||||
|
},
|
||||||
|
StripeConstants.SubscriptionStatus.Canceled => new InactiveSubscriptionWarning
|
||||||
|
{
|
||||||
|
Resolution = "resubscribe"
|
||||||
|
},
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InactiveSubscriptionWarning { Resolution = "contact_owner" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ResellerRenewalWarning?> GetResellerRenewalWarning(
|
||||||
|
Provider? provider,
|
||||||
|
Subscription subscription)
|
||||||
|
{
|
||||||
|
if (provider is not
|
||||||
|
{
|
||||||
|
Type: ProviderType.Reseller
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscription.CollectionMethod != StripeConstants.CollectionMethod.SendInvoice)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow;
|
||||||
|
|
||||||
|
// ReSharper disable once ConvertIfStatementToSwitchStatement
|
||||||
|
if (subscription is
|
||||||
|
{
|
||||||
|
Status: StripeConstants.SubscriptionStatus.Trialing or StripeConstants.SubscriptionStatus.Active,
|
||||||
|
LatestInvoice: null or { Status: StripeConstants.InvoiceStatus.Paid }
|
||||||
|
} && (subscription.CurrentPeriodEnd - now).TotalDays <= 14)
|
||||||
|
{
|
||||||
|
return new ResellerRenewalWarning
|
||||||
|
{
|
||||||
|
Type = "upcoming",
|
||||||
|
Upcoming = new ResellerRenewalWarning.UpcomingRenewal
|
||||||
|
{
|
||||||
|
RenewalDate = subscription.CurrentPeriodEnd
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscription is
|
||||||
|
{
|
||||||
|
Status: StripeConstants.SubscriptionStatus.Active,
|
||||||
|
LatestInvoice: { Status: StripeConstants.InvoiceStatus.Open, DueDate: not null }
|
||||||
|
} && subscription.LatestInvoice.DueDate > now)
|
||||||
|
{
|
||||||
|
return new ResellerRenewalWarning
|
||||||
|
{
|
||||||
|
Type = "issued",
|
||||||
|
Issued = new ResellerRenewalWarning.IssuedRenewal
|
||||||
|
{
|
||||||
|
IssuedDate = subscription.LatestInvoice.Created,
|
||||||
|
DueDate = subscription.LatestInvoice.DueDate.Value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReSharper disable once InvertIf
|
||||||
|
if (subscription.Status == StripeConstants.SubscriptionStatus.PastDue)
|
||||||
|
{
|
||||||
|
var openInvoices = await stripeAdapter.InvoiceSearchAsync(new InvoiceSearchOptions
|
||||||
|
{
|
||||||
|
Query = $"subscription:'{subscription.Id}' status:'open'"
|
||||||
|
});
|
||||||
|
|
||||||
|
var earliestOverdueInvoice = openInvoices
|
||||||
|
.Where(invoice => invoice.DueDate != null && invoice.DueDate < now)
|
||||||
|
.MinBy(invoice => invoice.Created);
|
||||||
|
|
||||||
|
if (earliestOverdueInvoice != null)
|
||||||
|
{
|
||||||
|
return new ResellerRenewalWarning
|
||||||
|
{
|
||||||
|
Type = "past_due",
|
||||||
|
PastDue = new ResellerRenewalWarning.PastDueRenewal
|
||||||
|
{
|
||||||
|
SuspensionDate = earliestOverdueInvoice.DueDate!.Value.AddDays(30)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
11
src/Api/Billing/Registrations.cs
Normal file
11
src/Api/Billing/Registrations.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using Bit.Api.Billing.Queries.Organizations;
|
||||||
|
|
||||||
|
namespace Bit.Api.Billing;
|
||||||
|
|
||||||
|
public static class Registrations
|
||||||
|
{
|
||||||
|
public static void AddBillingQueries(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddTransient<IOrganizationWarningsQuery, OrganizationWarningsQuery>();
|
||||||
|
}
|
||||||
|
}
|
34
src/Api/Controllers/PhishingDomainsController.cs
Normal file
34
src/Api/Controllers/PhishingDomainsController.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bit.Api.Controllers;
|
||||||
|
|
||||||
|
[Route("phishing-domains")]
|
||||||
|
public class PhishingDomainsController(IPhishingDomainRepository phishingDomainRepository, IFeatureService featureService) : Controller
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<ICollection<string>>> GetPhishingDomainsAsync()
|
||||||
|
{
|
||||||
|
if (!featureService.IsEnabled(FeatureFlagKeys.PhishingDetection))
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var domains = await phishingDomainRepository.GetActivePhishingDomainsAsync();
|
||||||
|
return Ok(domains);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("checksum")]
|
||||||
|
public async Task<ActionResult<string>> GetChecksumAsync()
|
||||||
|
{
|
||||||
|
if (!featureService.IsEnabled(FeatureFlagKeys.PhishingDetection))
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var checksum = await phishingDomainRepository.GetCurrentChecksumAsync();
|
||||||
|
return Ok(checksum);
|
||||||
|
}
|
||||||
|
}
|
@ -4,18 +4,20 @@ namespace Bit.Api.Tools.Models.Response;
|
|||||||
|
|
||||||
public class MemberCipherDetailsResponseModel
|
public class MemberCipherDetailsResponseModel
|
||||||
{
|
{
|
||||||
|
public Guid? UserGuid { get; set; }
|
||||||
public string UserName { get; set; }
|
public string UserName { get; set; }
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
public bool UsesKeyConnector { get; set; }
|
public bool UsesKeyConnector { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A distinct list of the cipher ids associated with
|
/// A distinct list of the cipher ids associated with
|
||||||
/// the organization member
|
/// the organization member
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<string> CipherIds { get; set; }
|
public IEnumerable<string> CipherIds { get; set; }
|
||||||
|
|
||||||
public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails)
|
public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails)
|
||||||
{
|
{
|
||||||
|
this.UserGuid = memberAccessCipherDetails.UserGuid;
|
||||||
this.UserName = memberAccessCipherDetails.UserName;
|
this.UserName = memberAccessCipherDetails.UserName;
|
||||||
this.Email = memberAccessCipherDetails.Email;
|
this.Email = memberAccessCipherDetails.Email;
|
||||||
this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector;
|
this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector;
|
@ -58,6 +58,13 @@ public class JobsHostedService : BaseJobsHostedService
|
|||||||
.StartNow()
|
.StartNow()
|
||||||
.WithCronSchedule("0 0 * * * ?")
|
.WithCronSchedule("0 0 * * * ?")
|
||||||
.Build();
|
.Build();
|
||||||
|
var updatePhishingDomainsTrigger = TriggerBuilder.Create()
|
||||||
|
.WithIdentity("UpdatePhishingDomainsTrigger")
|
||||||
|
.StartNow()
|
||||||
|
.WithSimpleSchedule(x => x
|
||||||
|
.WithIntervalInHours(24)
|
||||||
|
.RepeatForever())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
|
||||||
var jobs = new List<Tuple<Type, ITrigger>>
|
var jobs = new List<Tuple<Type, ITrigger>>
|
||||||
@ -68,6 +75,7 @@ public class JobsHostedService : BaseJobsHostedService
|
|||||||
new Tuple<Type, ITrigger>(typeof(ValidateUsersJob), everyTopOfTheSixthHourTrigger),
|
new Tuple<Type, ITrigger>(typeof(ValidateUsersJob), everyTopOfTheSixthHourTrigger),
|
||||||
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger),
|
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger),
|
||||||
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationDomainJob), validateOrganizationDomainTrigger),
|
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationDomainJob), validateOrganizationDomainTrigger),
|
||||||
|
new Tuple<Type, ITrigger>(typeof(UpdatePhishingDomainsJob), updatePhishingDomainsTrigger),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (_globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication)
|
if (_globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication)
|
||||||
@ -96,6 +104,7 @@ public class JobsHostedService : BaseJobsHostedService
|
|||||||
services.AddTransient<ValidateUsersJob>();
|
services.AddTransient<ValidateUsersJob>();
|
||||||
services.AddTransient<ValidateOrganizationsJob>();
|
services.AddTransient<ValidateOrganizationsJob>();
|
||||||
services.AddTransient<ValidateOrganizationDomainJob>();
|
services.AddTransient<ValidateOrganizationDomainJob>();
|
||||||
|
services.AddTransient<UpdatePhishingDomainsJob>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddCommercialSecretsManagerJobServices(IServiceCollection services)
|
public static void AddCommercialSecretsManagerJobServices(IServiceCollection services)
|
||||||
|
97
src/Api/Jobs/UpdatePhishingDomainsJob.cs
Normal file
97
src/Api/Jobs/UpdatePhishingDomainsJob.cs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Jobs;
|
||||||
|
using Bit.Core.PhishingDomainFeatures.Interfaces;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Quartz;
|
||||||
|
|
||||||
|
namespace Bit.Api.Jobs;
|
||||||
|
|
||||||
|
public class UpdatePhishingDomainsJob : BaseJob
|
||||||
|
{
|
||||||
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
private readonly IPhishingDomainRepository _phishingDomainRepository;
|
||||||
|
private readonly ICloudPhishingDomainQuery _cloudPhishingDomainQuery;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
public UpdatePhishingDomainsJob(
|
||||||
|
GlobalSettings globalSettings,
|
||||||
|
IPhishingDomainRepository phishingDomainRepository,
|
||||||
|
ICloudPhishingDomainQuery cloudPhishingDomainQuery,
|
||||||
|
IFeatureService featureService,
|
||||||
|
ILogger<UpdatePhishingDomainsJob> logger)
|
||||||
|
: base(logger)
|
||||||
|
{
|
||||||
|
_globalSettings = globalSettings;
|
||||||
|
_phishingDomainRepository = phishingDomainRepository;
|
||||||
|
_cloudPhishingDomainQuery = cloudPhishingDomainQuery;
|
||||||
|
_featureService = featureService;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteJobAsync(IJobExecutionContext context)
|
||||||
|
{
|
||||||
|
if (!_featureService.IsEnabled(FeatureFlagKeys.PhishingDetection))
|
||||||
|
{
|
||||||
|
_logger.LogInformation(Constants.BypassFiltersEventId, "Skipping phishing domain update. Feature flag is disabled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_globalSettings.PhishingDomain?.UpdateUrl))
|
||||||
|
{
|
||||||
|
_logger.LogInformation(Constants.BypassFiltersEventId, "Skipping phishing domain update. No URL configured.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_globalSettings.SelfHosted && !_globalSettings.EnableCloudCommunication)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(Constants.BypassFiltersEventId, "Skipping phishing domain update. Cloud communication is disabled in global settings.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var remoteChecksum = await _cloudPhishingDomainQuery.GetRemoteChecksumAsync();
|
||||||
|
if (string.IsNullOrWhiteSpace(remoteChecksum))
|
||||||
|
{
|
||||||
|
_logger.LogWarning(Constants.BypassFiltersEventId, "Could not retrieve remote checksum. Skipping update.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentChecksum = await _phishingDomainRepository.GetCurrentChecksumAsync();
|
||||||
|
|
||||||
|
if (string.Equals(currentChecksum, remoteChecksum, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_logger.LogInformation(Constants.BypassFiltersEventId,
|
||||||
|
"Phishing domains list is up to date (checksum: {Checksum}). Skipping update.",
|
||||||
|
currentChecksum);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(Constants.BypassFiltersEventId,
|
||||||
|
"Checksums differ (current: {CurrentChecksum}, remote: {RemoteChecksum}). Fetching updated domains from {Source}.",
|
||||||
|
currentChecksum, remoteChecksum, _globalSettings.SelfHosted ? "Bitwarden cloud API" : "external source");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var domains = await _cloudPhishingDomainQuery.GetPhishingDomainsAsync();
|
||||||
|
if (!domains.Contains("phishing.testcategory.com", StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
domains.Add("phishing.testcategory.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domains.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(Constants.BypassFiltersEventId, "Updating {Count} phishing domains with checksum {Checksum}.",
|
||||||
|
domains.Count, remoteChecksum);
|
||||||
|
await _phishingDomainRepository.UpdatePhishingDomainsAsync(domains, remoteChecksum);
|
||||||
|
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully updated phishing domains.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning(Constants.BypassFiltersEventId, "No valid domains found in the response. Skipping update.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(Constants.BypassFiltersEventId, ex, "Error updating phishing domains.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,7 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
|
|||||||
using Bit.Core.Tools.Entities;
|
using Bit.Core.Tools.Entities;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||||
|
using Bit.Api.Billing;
|
||||||
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
using Bit.Core.Auth.Identity.TokenProviders;
|
using Bit.Core.Auth.Identity.TokenProviders;
|
||||||
@ -182,6 +183,9 @@ public class Startup
|
|||||||
services.AddBillingOperations();
|
services.AddBillingOperations();
|
||||||
services.AddReportingServices();
|
services.AddReportingServices();
|
||||||
services.AddImportServices();
|
services.AddImportServices();
|
||||||
|
services.AddPhishingDomainServices(globalSettings);
|
||||||
|
|
||||||
|
services.AddBillingQueries();
|
||||||
|
|
||||||
// Authorization Handlers
|
// Authorization Handlers
|
||||||
services.AddAuthorizationHandlers();
|
services.AddAuthorizationHandlers();
|
||||||
|
@ -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}")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,6 +3,10 @@ using Bit.Api.Tools.Authorization;
|
|||||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
||||||
using Bit.Core.IdentityServer;
|
using Bit.Core.IdentityServer;
|
||||||
|
using Bit.Core.PhishingDomainFeatures;
|
||||||
|
using Bit.Core.PhishingDomainFeatures.Interfaces;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Repositories.Implementations;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Vault.Authorization.SecurityTasks;
|
using Bit.Core.Vault.Authorization.SecurityTasks;
|
||||||
@ -109,4 +113,25 @@ public static class ServiceCollectionExtensions
|
|||||||
|
|
||||||
services.AddScoped<IAuthorizationHandler, OrganizationRequirementHandler>();
|
services.AddScoped<IAuthorizationHandler, OrganizationRequirementHandler>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void AddPhishingDomainServices(this IServiceCollection services, GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
services.AddHttpClient("PhishingDomains", client =>
|
||||||
|
{
|
||||||
|
client.DefaultRequestHeaders.Add("User-Agent", globalSettings.SelfHosted ? "Bitwarden Self-Hosted" : "Bitwarden");
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(1000); // the source list is very slow
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddSingleton<AzurePhishingDomainStorageService>();
|
||||||
|
services.AddSingleton<IPhishingDomainRepository, AzurePhishingDomainRepository>();
|
||||||
|
|
||||||
|
if (globalSettings.SelfHosted)
|
||||||
|
{
|
||||||
|
services.AddScoped<ICloudPhishingDomainQuery, CloudPhishingDomainRelayQuery>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
services.AddScoped<ICloudPhishingDomainQuery, CloudPhishingDomainDirectQuery>();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1086,9 +1086,8 @@ public class CiphersController : Controller
|
|||||||
throw new BadRequestException(ModelState);
|
throw new BadRequestException(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization.
|
// Check if the user is claimed by any organization.
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||||
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details.");
|
throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details.");
|
||||||
}
|
}
|
||||||
@ -1241,6 +1240,20 @@ public class CiphersController : Controller
|
|||||||
return new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp);
|
return new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}/attachment/{attachmentId}/admin")]
|
||||||
|
public async Task<AttachmentResponseModel> GetAttachmentDataAdmin(Guid id, string attachmentId)
|
||||||
|
{
|
||||||
|
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id);
|
||||||
|
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
||||||
|
!await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _cipherService.GetAttachmentDownloadDataAsync(cipher, attachmentId);
|
||||||
|
return new AttachmentResponseModel(result);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/attachment/{attachmentId}")]
|
[HttpGet("{id}/attachment/{attachmentId}")]
|
||||||
public async Task<AttachmentResponseModel> GetAttachmentData(Guid id, string attachmentId)
|
public async Task<AttachmentResponseModel> GetAttachmentData(Guid id, string attachmentId)
|
||||||
{
|
{
|
||||||
@ -1287,18 +1300,17 @@ public class CiphersController : Controller
|
|||||||
|
|
||||||
[HttpDelete("{id}/attachment/{attachmentId}/admin")]
|
[HttpDelete("{id}/attachment/{attachmentId}/admin")]
|
||||||
[HttpPost("{id}/attachment/{attachmentId}/delete-admin")]
|
[HttpPost("{id}/attachment/{attachmentId}/delete-admin")]
|
||||||
public async Task DeleteAttachmentAdmin(string id, string attachmentId)
|
public async Task<DeleteAttachmentResponseData> DeleteAttachmentAdmin(Guid id, string attachmentId)
|
||||||
{
|
{
|
||||||
var idGuid = new Guid(id);
|
|
||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
var cipher = await _cipherRepository.GetByIdAsync(idGuid);
|
var cipher = await _cipherRepository.GetByIdAsync(id);
|
||||||
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
||||||
!await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))
|
!await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, true);
|
return await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
|
@ -3,6 +3,7 @@ using Bit.Core;
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -37,6 +38,7 @@ public class SyncController : Controller
|
|||||||
private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion);
|
private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion);
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
|
|
||||||
public SyncController(
|
public SyncController(
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
@ -51,7 +53,8 @@ public class SyncController : Controller
|
|||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IApplicationCacheService applicationCacheService)
|
IApplicationCacheService applicationCacheService,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_folderRepository = folderRepository;
|
_folderRepository = folderRepository;
|
||||||
@ -66,6 +69,7 @@ public class SyncController : Controller
|
|||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
@ -102,7 +106,7 @@ public class SyncController : Controller
|
|||||||
collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
|
collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
|
||||||
}
|
}
|
||||||
|
|
||||||
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(user.Id);
|
var organizationClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(user.Id);
|
||||||
var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id);
|
var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id);
|
||||||
|
@ -37,6 +37,10 @@
|
|||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
"connectionString": "UseDevelopmentStorage=true"
|
"connectionString": "UseDevelopmentStorage=true"
|
||||||
|
},
|
||||||
|
"phishingDomain": {
|
||||||
|
"updateUrl": "https://phish.co.za/latest/phishing-domains-ACTIVE.txt",
|
||||||
|
"checksumUrl": "https://raw.githubusercontent.com/Phishing-Database/checksums/refs/heads/master/phishing-domains-ACTIVE.txt.sha256"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,6 +71,9 @@
|
|||||||
"accessKeySecret": "SECRET",
|
"accessKeySecret": "SECRET",
|
||||||
"region": "SECRET"
|
"region": "SECRET"
|
||||||
},
|
},
|
||||||
|
"phishingDomain": {
|
||||||
|
"updateUrl": "SECRET"
|
||||||
|
},
|
||||||
"distributedIpRateLimiting": {
|
"distributedIpRateLimiting": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"maxRedisTimeoutsThreshold": 10,
|
"maxRedisTimeoutsThreshold": 10,
|
||||||
|
@ -63,6 +63,12 @@ public class FreshdeskController : Controller
|
|||||||
note += $"<li>Region: {_billingSettings.FreshDesk.Region}</li>";
|
note += $"<li>Region: {_billingSettings.FreshDesk.Region}</li>";
|
||||||
var customFields = new Dictionary<string, object>();
|
var customFields = new Dictionary<string, object>();
|
||||||
var user = await _userRepository.GetByEmailAsync(ticketContactEmail);
|
var user = await _userRepository.GetByEmailAsync(ticketContactEmail);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
note += $"<li>No user found: {ticketContactEmail}</li>";
|
||||||
|
await CreateNote(ticketId, note);
|
||||||
|
}
|
||||||
|
|
||||||
if (user != null)
|
if (user != null)
|
||||||
{
|
{
|
||||||
var userLink = $"{_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}";
|
var userLink = $"{_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}";
|
||||||
@ -121,18 +127,7 @@ public class FreshdeskController : Controller
|
|||||||
Content = JsonContent.Create(updateBody),
|
Content = JsonContent.Create(updateBody),
|
||||||
};
|
};
|
||||||
await CallFreshdeskApiAsync(updateRequest);
|
await CallFreshdeskApiAsync(updateRequest);
|
||||||
|
await CreateNote(ticketId, note);
|
||||||
var noteBody = new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
{ "body", $"<ul>{note}</ul>" },
|
|
||||||
{ "private", true }
|
|
||||||
};
|
|
||||||
var noteRequest = new HttpRequestMessage(HttpMethod.Post,
|
|
||||||
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId))
|
|
||||||
{
|
|
||||||
Content = JsonContent.Create(noteBody),
|
|
||||||
};
|
|
||||||
await CallFreshdeskApiAsync(noteRequest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new OkResult();
|
return new OkResult();
|
||||||
@ -208,6 +203,21 @@ public class FreshdeskController : Controller
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CreateNote(string ticketId, string note)
|
||||||
|
{
|
||||||
|
var noteBody = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "body", $"<ul>{note}</ul>" },
|
||||||
|
{ "private", true }
|
||||||
|
};
|
||||||
|
var noteRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||||
|
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId))
|
||||||
|
{
|
||||||
|
Content = JsonContent.Create(noteBody),
|
||||||
|
};
|
||||||
|
await CallFreshdeskApiAsync(noteRequest);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task AddAnswerNoteToTicketAsync(string note, string ticketId)
|
private async Task AddAnswerNoteToTicketAsync(string note, string ticketId)
|
||||||
{
|
{
|
||||||
// if there is no content, then we don't need to add a note
|
// if there is no content, then we don't need to add a note
|
||||||
|
@ -4,8 +4,8 @@ using Bit.Core.Billing.Constants;
|
|||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
|
using Bit.Core.Billing.Tax.Services;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.Data.Integrations;
|
||||||
|
|
||||||
|
public class IntegrationTemplateContext(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
public EventMessage Event { get; } = eventMessage;
|
||||||
|
|
||||||
|
public string DomainName => Event.DomainName;
|
||||||
|
public string IpAddress => Event.IpAddress;
|
||||||
|
public DeviceType? DeviceType => Event.DeviceType;
|
||||||
|
public Guid? ActingUserId => Event.ActingUserId;
|
||||||
|
public Guid? OrganizationUserId => Event.OrganizationUserId;
|
||||||
|
public DateTime Date => Event.Date;
|
||||||
|
public EventType Type => Event.Type;
|
||||||
|
public Guid? UserId => Event.UserId;
|
||||||
|
public Guid? OrganizationId => Event.OrganizationId;
|
||||||
|
public Guid? CipherId => Event.CipherId;
|
||||||
|
public Guid? CollectionId => Event.CollectionId;
|
||||||
|
public Guid? GroupId => Event.GroupId;
|
||||||
|
public Guid? PolicyId => Event.PolicyId;
|
||||||
|
|
||||||
|
public User? User { get; set; }
|
||||||
|
public string? UserName => User?.Name;
|
||||||
|
public string? UserEmail => User?.Email;
|
||||||
|
|
||||||
|
public User? ActingUser { get; set; }
|
||||||
|
public string? ActingUserName => ActingUser?.Name;
|
||||||
|
public string? ActingUserEmail => ActingUser?.Email;
|
||||||
|
|
||||||
|
public Organization? Organization { get; set; }
|
||||||
|
public string? OrganizationName => Organization?.DisplayName();
|
||||||
|
}
|
@ -60,4 +60,5 @@ public class OrganizationUserOrganizationDetails
|
|||||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||||
public bool UseRiskInsights { get; set; }
|
public bool UseRiskInsights { get; set; }
|
||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
|
public bool? IsAdminInitiated { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@ public class VerifyOrganizationDomainCommand(
|
|||||||
IDnsResolverService dnsResolverService,
|
IDnsResolverService dnsResolverService,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
IFeatureService featureService,
|
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
ISavePolicyCommand savePolicyCommand,
|
ISavePolicyCommand savePolicyCommand,
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
@ -125,11 +124,8 @@ public class VerifyOrganizationDomainCommand(
|
|||||||
|
|
||||||
private async Task DomainVerificationSideEffectsAsync(OrganizationDomain domain, IActingUser actingUser)
|
private async Task DomainVerificationSideEffectsAsync(OrganizationDomain domain, IActingUser actingUser)
|
||||||
{
|
{
|
||||||
if (featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser);
|
||||||
{
|
await SendVerifiedDomainUserEmailAsync(domain);
|
||||||
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser);
|
|
||||||
await SendVerifiedDomainUserEmailAsync(domain);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser) =>
|
private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser) =>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -24,6 +25,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
|||||||
private readonly IPolicyService _policyService;
|
private readonly IPolicyService _policyService;
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||||
|
|
||||||
public AcceptOrgUserCommand(
|
public AcceptOrgUserCommand(
|
||||||
@ -34,6 +36,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
|||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
|
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
|
||||||
{
|
{
|
||||||
|
|
||||||
@ -45,6 +48,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
|||||||
_policyService = policyService;
|
_policyService = policyService;
|
||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,7 +196,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enforce Two Factor Authentication Policy of organization user is trying to join
|
// Enforce Two Factor Authentication Policy of organization user is trying to join
|
||||||
if (!await userService.TwoFactorIsEnabledAsync(user))
|
if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
|
||||||
{
|
{
|
||||||
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
|
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
|
||||||
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
|
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
|
||||||
|
@ -6,4 +6,8 @@ namespace Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
|||||||
public interface IOrganizationUserUserDetailsQuery
|
public interface IOrganizationUserUserDetailsQuery
|
||||||
{
|
{
|
||||||
Task<IEnumerable<OrganizationUserUserDetails>> GetOrganizationUserUserDetails(OrganizationUserUserDetailsQueryRequest request);
|
Task<IEnumerable<OrganizationUserUserDetails>> GetOrganizationUserUserDetails(OrganizationUserUserDetailsQueryRequest request);
|
||||||
|
|
||||||
|
Task<IEnumerable<(OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization)>> Get(OrganizationUserUserDetailsQueryRequest request);
|
||||||
|
|
||||||
|
Task<IEnumerable<(OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization)>> GetAccountRecoveryEnrolledUsers(OrganizationUserUserDetailsQueryRequest request);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||||
using Bit.Core.Models.Commands;
|
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
using Bit.Core.Models.Commands;
|
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||||
|
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Errors;
|
|
||||||
using Bit.Core.AdminConsole.Interfaces;
|
using Bit.Core.AdminConsole.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Models.Business;
|
using Bit.Core.AdminConsole.Models.Business;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Shared.Validation;
|
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||||
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Models.Commands;
|
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -50,11 +50,11 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
|||||||
{
|
{
|
||||||
case Failure<InviteOrganizationUsersResponse> failure:
|
case Failure<InviteOrganizationUsersResponse> failure:
|
||||||
return new Failure<ScimInviteOrganizationUsersResponse>(
|
return new Failure<ScimInviteOrganizationUsersResponse>(
|
||||||
failure.Errors.Select(error => new Error<ScimInviteOrganizationUsersResponse>(error.Message,
|
new Error<ScimInviteOrganizationUsersResponse>(failure.Error.Message,
|
||||||
new ScimInviteOrganizationUsersResponse
|
new ScimInviteOrganizationUsersResponse
|
||||||
{
|
{
|
||||||
InvitedUser = error.ErroredValue.InvitedUsers.FirstOrDefault()
|
InvitedUser = failure.Error.ErroredValue.InvitedUsers.FirstOrDefault()
|
||||||
})));
|
}));
|
||||||
|
|
||||||
case Success<InviteOrganizationUsersResponse> success when success.Value.InvitedUsers.Any():
|
case Success<InviteOrganizationUsersResponse> success when success.Value.InvitedUsers.Any():
|
||||||
var user = success.Value.InvitedUsers.First();
|
var user = success.Value.InvitedUsers.First();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.AdminConsole.Shared.Validation;
|
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
|
||||||
using Bit.Core.AdminConsole.Shared.Validation;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.Models.Business;
|
||||||
using Bit.Core.AdminConsole.Models.Business;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Models.Business;
|
using Bit.Core.AdminConsole.Models.Business;
|
||||||
using Bit.Core.AdminConsole.Shared.Validation;
|
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.V
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Shared.Validation;
|
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
|
||||||
using Bit.Core.AdminConsole.Shared.Validation;
|
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Shared.Validation;
|
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||||
@ -9,12 +13,21 @@ namespace Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
|||||||
public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuery
|
public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuery
|
||||||
{
|
{
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly IGetOrganizationUsersClaimedStatusQuery _getOrganizationUsersClaimedStatusQuery;
|
||||||
|
|
||||||
public OrganizationUserUserDetailsQuery(
|
public OrganizationUserUserDetailsQuery(
|
||||||
IOrganizationUserRepository organizationUserRepository
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
|
IFeatureService featureService,
|
||||||
|
IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
|
_featureService = featureService;
|
||||||
|
_getOrganizationUsersClaimedStatusQuery = getOrganizationUsersClaimedStatusQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -37,4 +50,42 @@ public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuer
|
|||||||
return o;
|
return o;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the organization user user details, two factor enabled status, and
|
||||||
|
/// claimed status for the provided request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request details for the query</param>
|
||||||
|
/// <returns>List of OrganizationUserUserDetails</returns>
|
||||||
|
public async Task<IEnumerable<(OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization)>> Get(OrganizationUserUserDetailsQueryRequest request)
|
||||||
|
{
|
||||||
|
var organizationUsers = await GetOrganizationUserUserDetails(request);
|
||||||
|
|
||||||
|
var organizationUsersTwoFactorEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers)).ToDictionary(u => u.user.Id);
|
||||||
|
var organizationUsersClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(request.OrganizationId, organizationUsers.Select(o => o.Id));
|
||||||
|
var responses = organizationUsers.Select(o => (o, organizationUsersTwoFactorEnabled[o.Id].twoFactorIsEnabled, organizationUsersClaimedStatus[o.Id]));
|
||||||
|
|
||||||
|
|
||||||
|
return responses;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the organization users user details, two factor enabled status, and
|
||||||
|
/// claimed status for confirmed users that are enrolled in account recovery
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request details for the query</param>
|
||||||
|
/// <returns>List of OrganizationUserUserDetails</returns>
|
||||||
|
public async Task<IEnumerable<(OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization)>> GetAccountRecoveryEnrolledUsers(OrganizationUserUserDetailsQueryRequest request)
|
||||||
|
{
|
||||||
|
var organizationUsers = (await GetOrganizationUserUserDetails(request))
|
||||||
|
.Where(o => o.Status.Equals(OrganizationUserStatusType.Confirmed) && o.UsesKeyConnector == false && !String.IsNullOrEmpty(o.ResetPasswordKey));
|
||||||
|
|
||||||
|
var organizationUsersTwoFactorEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers)).ToDictionary(u => u.user.Id);
|
||||||
|
var organizationUsersClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(request.OrganizationId, organizationUsers.Select(o => o.Id));
|
||||||
|
var responses = organizationUsers
|
||||||
|
.Select(o => (o, organizationUsersTwoFactorEnabled[o.Id].twoFactorIsEnabled, organizationUsersClaimedStatus[o.Id]));
|
||||||
|
|
||||||
|
return responses;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -159,7 +159,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
|
|||||||
throw new BadRequestException(RemoveAdminByCustomUserErrorMessage);
|
throw new BadRequestException(RemoveAdminByCustomUserErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null)
|
if (deletingUserId.HasValue && eventSystemUser == null)
|
||||||
{
|
{
|
||||||
var claimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgUser.OrganizationId, new[] { orgUser.Id });
|
var claimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgUser.OrganizationId, new[] { orgUser.Id });
|
||||||
if (claimedStatus.TryGetValue(orgUser.Id, out var isClaimed) && isClaimed)
|
if (claimedStatus.TryGetValue(orgUser.Id, out var isClaimed) && isClaimed)
|
||||||
@ -214,7 +214,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
|
|||||||
deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);
|
deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var claimedStatus = _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null
|
var claimedStatus = deletingUserId.HasValue && eventSystemUser == null
|
||||||
? await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(organizationId, filteredUsers.Select(u => u.Id))
|
? await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(organizationId, filteredUsers.Select(u => u.Id))
|
||||||
: filteredUsers.ToDictionary(u => u.Id, u => false);
|
: filteredUsers.ToDictionary(u => u.Id, u => false);
|
||||||
var result = new List<(OrganizationUser OrganizationUser, string ErrorMessage)>();
|
var result = new List<(OrganizationUser OrganizationUser, string ErrorMessage)>();
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
using Bit.Core.AdminConsole.Models.Data;
|
using Bit.Core.AdminConsole.Models.Data;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||||
|
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Commands;
|
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
@ -0,0 +1,128 @@
|
|||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Services;
|
||||||
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Tokens;
|
||||||
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||||
|
|
||||||
|
public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
|
||||||
|
{
|
||||||
|
|
||||||
|
private readonly IOrganizationService _organizationService;
|
||||||
|
private readonly ICollectionRepository _collectionRepository;
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||||
|
private readonly IDataProtector _dataProtector;
|
||||||
|
private readonly IGlobalSettings _globalSettings;
|
||||||
|
private readonly IPolicyService _policyService;
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
|
||||||
|
public InitPendingOrganizationCommand(
|
||||||
|
IOrganizationService organizationService,
|
||||||
|
ICollectionRepository collectionRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
||||||
|
IDataProtectionProvider dataProtectionProvider,
|
||||||
|
IGlobalSettings globalSettings,
|
||||||
|
IPolicyService policyService,
|
||||||
|
IOrganizationUserRepository organizationUserRepository
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_organizationService = organizationService;
|
||||||
|
_collectionRepository = collectionRepository;
|
||||||
|
_organizationRepository = organizationRepository;
|
||||||
|
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||||
|
_dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose);
|
||||||
|
_globalSettings = globalSettings;
|
||||||
|
_policyService = policyService;
|
||||||
|
_organizationUserRepository = organizationUserRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InitPendingOrganizationAsync(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken)
|
||||||
|
{
|
||||||
|
await ValidateSignUpPoliciesAsync(user.Id);
|
||||||
|
|
||||||
|
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||||
|
if (orgUser == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("User invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenValid = ValidateInviteToken(orgUser, user, emailToken);
|
||||||
|
|
||||||
|
if (!tokenValid)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Invalid token");
|
||||||
|
}
|
||||||
|
|
||||||
|
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
|
||||||
|
if (org.Enabled)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Organization is already enabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (org.Status != OrganizationStatusType.Pending)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Organization is not on a Pending status.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(org.PublicKey))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Organization already has a Public Key.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(org.PrivateKey))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Organization already has a Private Key.");
|
||||||
|
}
|
||||||
|
|
||||||
|
org.Enabled = true;
|
||||||
|
org.Status = OrganizationStatusType.Created;
|
||||||
|
org.PublicKey = publicKey;
|
||||||
|
org.PrivateKey = privateKey;
|
||||||
|
|
||||||
|
await _organizationService.UpdateAsync(org);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(collectionName))
|
||||||
|
{
|
||||||
|
// give the owner Can Manage access over the default collection
|
||||||
|
List<CollectionAccessSelection> defaultOwnerAccess =
|
||||||
|
[new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }];
|
||||||
|
|
||||||
|
var defaultCollection = new Collection
|
||||||
|
{
|
||||||
|
Name = collectionName,
|
||||||
|
OrganizationId = org.Id
|
||||||
|
};
|
||||||
|
await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
|
||||||
|
{
|
||||||
|
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
|
||||||
|
if (anySingleOrgPolicies)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You may not create an organization. You belong to an organization " +
|
||||||
|
"which has a policy that prohibits you from being a member of any other organization.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ValidateInviteToken(OrganizationUser orgUser, User user, string emailToken)
|
||||||
|
{
|
||||||
|
var tokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken(
|
||||||
|
_orgUserInviteTokenDataFactory, emailToken, orgUser);
|
||||||
|
|
||||||
|
return tokenValid;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
namespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
|
||||||
|
public interface IInitPendingOrganizationCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Update an Organization entry by setting the public/private keys, set it as 'Enabled' and move the Status from 'Pending' to 'Created'.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method must target a disabled Organization that has null keys and status as 'Pending'.
|
||||||
|
/// </remarks>
|
||||||
|
Task InitPendingOrganizationAsync(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken);
|
||||||
|
}
|
@ -61,16 +61,9 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
|||||||
{
|
{
|
||||||
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
||||||
{
|
{
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
var currentUser = _currentContext.UserId ?? Guid.Empty;
|
||||||
{
|
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
|
||||||
var currentUser = _currentContext.UserId ?? Guid.Empty;
|
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
||||||
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
|
|
||||||
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,42 +109,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
|||||||
_mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email)));
|
_mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
|
|
||||||
{
|
|
||||||
// Remove non-compliant users
|
|
||||||
var savingUserId = _currentContext.UserId;
|
|
||||||
// Note: must get OrganizationUserUserDetails so that Email is always populated from the User object
|
|
||||||
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
|
||||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
|
||||||
if (org == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException(OrganizationNotFoundErrorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
var removableOrgUsers = orgUsers.Where(ou =>
|
|
||||||
ou.Status != OrganizationUserStatusType.Invited &&
|
|
||||||
ou.Status != OrganizationUserStatusType.Revoked &&
|
|
||||||
ou.Type != OrganizationUserType.Owner &&
|
|
||||||
ou.Type != OrganizationUserType.Admin &&
|
|
||||||
ou.UserId != savingUserId
|
|
||||||
).ToList();
|
|
||||||
|
|
||||||
var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(
|
|
||||||
removableOrgUsers.Select(ou => ou.UserId!.Value));
|
|
||||||
foreach (var orgUser in removableOrgUsers)
|
|
||||||
{
|
|
||||||
if (userOrgs.Any(ou => ou.UserId == orgUser.UserId
|
|
||||||
&& ou.OrganizationId != org.Id
|
|
||||||
&& ou.Status != OrganizationUserStatusType.Invited))
|
|
||||||
{
|
|
||||||
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id, savingUserId);
|
|
||||||
|
|
||||||
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
|
|
||||||
org.DisplayName(), orgUser.Email);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||||
{
|
{
|
||||||
if (policyUpdate is not { Enabled: true })
|
if (policyUpdate is not { Enabled: true })
|
||||||
@ -165,8 +122,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
|||||||
return validateDecryptionErrorMessage;
|
return validateDecryptionErrorMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
if (await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))
|
||||||
&& await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))
|
|
||||||
{
|
{
|
||||||
return ClaimedDomainSingleOrganizationRequiredErrorMessage;
|
return ClaimedDomainSingleOrganizationRequiredErrorMessage;
|
||||||
}
|
}
|
||||||
|
@ -23,8 +23,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
|||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
|
||||||
private readonly IFeatureService _featureService;
|
|
||||||
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
|
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
|
||||||
|
|
||||||
public const string NonCompliantMembersWillLoseAccessMessage = "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.";
|
public const string NonCompliantMembersWillLoseAccessMessage = "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.";
|
||||||
@ -38,8 +36,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
|||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
|
||||||
IFeatureService featureService,
|
|
||||||
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
|
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
|
||||||
{
|
{
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@ -47,8 +43,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
|||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
|
||||||
_featureService = featureService;
|
|
||||||
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,16 +50,9 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
|||||||
{
|
{
|
||||||
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
||||||
{
|
{
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
var currentUser = _currentContext.UserId ?? Guid.Empty;
|
||||||
{
|
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
|
||||||
var currentUser = _currentContext.UserId ?? Guid.Empty;
|
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
||||||
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
|
|
||||||
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,40 +108,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
|||||||
_mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), x.Email)));
|
_mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), x.Email)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
|
|
||||||
{
|
|
||||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
|
||||||
var savingUserId = _currentContext.UserId;
|
|
||||||
|
|
||||||
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
|
||||||
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);
|
|
||||||
var removableOrgUsers = orgUsers.Where(ou =>
|
|
||||||
ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked &&
|
|
||||||
ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin &&
|
|
||||||
ou.UserId != savingUserId);
|
|
||||||
|
|
||||||
// Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled
|
|
||||||
foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword))
|
|
||||||
{
|
|
||||||
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == orgUser.Id)
|
|
||||||
.twoFactorIsEnabled;
|
|
||||||
if (!userTwoFactorEnabled)
|
|
||||||
{
|
|
||||||
if (!orgUser.HasMasterPassword)
|
|
||||||
{
|
|
||||||
throw new BadRequestException(
|
|
||||||
"Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id,
|
|
||||||
savingUserId);
|
|
||||||
|
|
||||||
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
|
|
||||||
org!.DisplayName(), orgUser.Email);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool MembersWithNoMasterPasswordWillLoseAccess(
|
private static bool MembersWithNoMasterPasswordWillLoseAccess(
|
||||||
IEnumerable<OrganizationUserUserDetails> orgUserDetails,
|
IEnumerable<OrganizationUserUserDetails> orgUserDetails,
|
||||||
IEnumerable<(OrganizationUserUserDetails user, bool isTwoFactorEnabled)> organizationUsersTwoFactorEnabled) =>
|
IEnumerable<(OrganizationUserUserDetails user, bool isTwoFactorEnabled)> organizationUsersTwoFactorEnabled) =>
|
||||||
|
@ -48,14 +48,8 @@ public interface IOrganizationService
|
|||||||
Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid organizationId,
|
Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid organizationId,
|
||||||
IEnumerable<Guid> organizationUserIds, Guid? revokingUserId);
|
IEnumerable<Guid> organizationUserIds, Guid? revokingUserId);
|
||||||
Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted);
|
Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted);
|
||||||
/// <summary>
|
|
||||||
/// Update an Organization entry by setting the public/private keys, set it as 'Enabled' and move the Status from 'Pending' to 'Created'.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This method must target a disabled Organization that has null keys and status as 'Pending'.
|
|
||||||
/// </remarks>
|
|
||||||
Task InitPendingOrganization(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken);
|
|
||||||
Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null);
|
Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null);
|
||||||
|
Task<(bool canScale, string failureReason)> CanScaleAsync(Organization organization, int seatsToAdd);
|
||||||
|
|
||||||
void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade);
|
void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade);
|
||||||
void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade);
|
void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Business.Provider;
|
using Bit.Core.AdminConsole.Models.Business.Provider;
|
||||||
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
|
|
||||||
@ -7,7 +8,8 @@ namespace Bit.Core.AdminConsole.Services;
|
|||||||
|
|
||||||
public interface IProviderService
|
public interface IProviderService
|
||||||
{
|
{
|
||||||
Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo = null);
|
Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo,
|
||||||
|
TokenizedPaymentSource tokenizedPaymentSource = null);
|
||||||
Task UpdateAsync(Provider provider, bool updateBilling = false);
|
Task UpdateAsync(Provider provider, bool updateBilling = false);
|
||||||
|
|
||||||
Task<List<ProviderUser>> InviteUserAsync(ProviderUserInvite<string> invite);
|
Task<List<ProviderUser>> InviteUserAsync(ProviderUserInvite<string> invite);
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public class EventRouteService(
|
||||||
|
[FromKeyedServices("broadcast")] IEventWriteService broadcastEventWriteService,
|
||||||
|
[FromKeyedServices("storage")] IEventWriteService storageEventWriteService,
|
||||||
|
IFeatureService _featureService) : IEventWriteService
|
||||||
|
{
|
||||||
|
public async Task CreateAsync(IEvent e)
|
||||||
|
{
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations))
|
||||||
|
{
|
||||||
|
await broadcastEventWriteService.CreateAsync(e);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await storageEventWriteService.CreateAsync(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateManyAsync(IEnumerable<IEvent> e)
|
||||||
|
{
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations))
|
||||||
|
{
|
||||||
|
await broadcastEventWriteService.CreateManyAsync(e);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await storageEventWriteService.CreateManyAsync(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Bit.Core.AdminConsole.Utilities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Models.Data.Integrations;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public abstract class IntegrationEventHandlerBase(
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationIntegrationConfigurationRepository configurationRepository)
|
||||||
|
: IEventMessageHandler
|
||||||
|
{
|
||||||
|
public async Task HandleEventAsync(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var organizationId = eventMessage.OrganizationId ?? Guid.Empty;
|
||||||
|
var configurations = await configurationRepository.GetConfigurationDetailsAsync(
|
||||||
|
organizationId,
|
||||||
|
GetIntegrationType(),
|
||||||
|
eventMessage.Type);
|
||||||
|
|
||||||
|
foreach (var configuration in configurations)
|
||||||
|
{
|
||||||
|
var context = await BuildContextAsync(eventMessage, configuration.Template);
|
||||||
|
var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(configuration.Template, context);
|
||||||
|
|
||||||
|
await ProcessEventIntegrationAsync(configuration.MergedConfiguration, renderedTemplate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleManyEventsAsync(IEnumerable<EventMessage> eventMessages)
|
||||||
|
{
|
||||||
|
foreach (var eventMessage in eventMessages)
|
||||||
|
{
|
||||||
|
await HandleEventAsync(eventMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IntegrationTemplateContext> BuildContextAsync(EventMessage eventMessage, string template)
|
||||||
|
{
|
||||||
|
var context = new IntegrationTemplateContext(eventMessage);
|
||||||
|
|
||||||
|
if (IntegrationTemplateProcessor.TemplateRequiresUser(template) && eventMessage.UserId.HasValue)
|
||||||
|
{
|
||||||
|
context.User = await userRepository.GetByIdAsync(eventMessage.UserId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IntegrationTemplateProcessor.TemplateRequiresActingUser(template) && eventMessage.ActingUserId.HasValue)
|
||||||
|
{
|
||||||
|
context.ActingUser = await userRepository.GetByIdAsync(eventMessage.ActingUserId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IntegrationTemplateProcessor.TemplateRequiresOrganization(template) && eventMessage.OrganizationId.HasValue)
|
||||||
|
{
|
||||||
|
context.Organization = await organizationRepository.GetByIdAsync(eventMessage.OrganizationId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract IntegrationType GetIntegrationType();
|
||||||
|
|
||||||
|
protected abstract Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, string renderedTemplate);
|
||||||
|
}
|
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