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
|
||||
**/KeyManagement @bitwarden/team-key-management-dev
|
||||
|
||||
# Tools team
|
||||
**/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 @bitwarden/team-vault-dev
|
||||
**/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:",
|
||||
reviewers: ["team:dept-bre"],
|
||||
addLabels: ["hold"]
|
||||
addLabels: ["hold"],
|
||||
},
|
||||
{
|
||||
groupName: "dockerfile minor",
|
||||
@ -37,6 +37,16 @@
|
||||
matchManagers: ["github-actions"],
|
||||
matchUpdateTypes: ["minor"],
|
||||
},
|
||||
{
|
||||
// For any Microsoft.Extensions.* and Microsoft.AspNetCore.* packages, we want to create PRs for patch updates.
|
||||
// This overrides the default that ignores patch updates for nuget dependencies.
|
||||
matchPackageNames: [
|
||||
"/^Microsoft\\.Extensions\\./",
|
||||
"/^Microsoft\\.AspNetCore\\./",
|
||||
],
|
||||
matchUpdateTypes: ["patch"],
|
||||
dependencyDashboardApproval: false,
|
||||
},
|
||||
{
|
||||
matchManagers: ["dockerfile", "docker-compose"],
|
||||
commitMessagePrefix: "[deps] BRE:",
|
||||
@ -59,6 +69,7 @@
|
||||
"DuoUniversal",
|
||||
"Fido2.AspNet",
|
||||
"Duende.IdentityServer",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer",
|
||||
"Microsoft.Extensions.Identity.Stores",
|
||||
"Otp.NET",
|
||||
"Sustainsys.Saml2.AspNetCore2",
|
||||
@ -79,8 +90,6 @@
|
||||
"CsvHelper",
|
||||
"Kralizek.AutoFixture.Extensions.MockHttp",
|
||||
"Microsoft.AspNetCore.Mvc.Testing",
|
||||
"Microsoft.Extensions.Logging",
|
||||
"Microsoft.Extensions.Logging.Console",
|
||||
"Newtonsoft.Json",
|
||||
"NSubstitute",
|
||||
"Sentry.Serilog",
|
||||
@ -100,9 +109,9 @@
|
||||
reviewers: ["team:team-billing-dev"],
|
||||
},
|
||||
{
|
||||
matchPackagePatterns: ["^Microsoft.Extensions.Logging"],
|
||||
groupName: "Microsoft.Extensions.Logging",
|
||||
description: "Group Microsoft.Extensions.Logging to exclude them from the dotnet monorepo preset",
|
||||
matchPackageNames: ["/^Microsoft\\.EntityFrameworkCore\\./", "/^dotnet-ef/"],
|
||||
groupName: "EntityFrameworkCore",
|
||||
description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset",
|
||||
},
|
||||
{
|
||||
matchPackageNames: [
|
||||
@ -117,9 +126,6 @@
|
||||
"Microsoft.EntityFrameworkCore.Relational",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer",
|
||||
"Microsoft.Extensions.Caching.Cosmos",
|
||||
"Microsoft.Extensions.Caching.SqlServer",
|
||||
"Microsoft.Extensions.Caching.StackExchangeRedis",
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL",
|
||||
"Pomelo.EntityFrameworkCore.MySql",
|
||||
],
|
||||
@ -142,56 +148,40 @@
|
||||
"Azure.Messaging.ServiceBus",
|
||||
"Azure.Storage.Blobs",
|
||||
"Azure.Storage.Queues",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer",
|
||||
"LaunchDarkly.ServerSdk",
|
||||
"Microsoft.AspNetCore.Http",
|
||||
"Microsoft.AspNetCore.SignalR.Protocols.MessagePack",
|
||||
"Microsoft.AspNetCore.SignalR.StackExchangeRedis",
|
||||
"Microsoft.Extensions.Configuration.EnvironmentVariables",
|
||||
"Microsoft.Extensions.Configuration.UserSecrets",
|
||||
"Microsoft.Extensions.Configuration",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions",
|
||||
"Microsoft.Extensions.DependencyInjection",
|
||||
"Microsoft.Extensions.Logging",
|
||||
"Microsoft.Extensions.Logging.Console",
|
||||
"Microsoft.Extensions.Caching.Cosmos",
|
||||
"Microsoft.Extensions.Caching.SqlServer",
|
||||
"Microsoft.Extensions.Caching.StackExchangeRedis",
|
||||
"Quartz",
|
||||
],
|
||||
description: "Platform owned dependencies",
|
||||
commitMessagePrefix: "[deps] Platform:",
|
||||
reviewers: ["team:team-platform-dev"],
|
||||
},
|
||||
{
|
||||
matchPackagePatterns: ["EntityFrameworkCore", "^dotnet-ef"],
|
||||
groupName: "EntityFrameworkCore",
|
||||
description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset",
|
||||
},
|
||||
{
|
||||
matchPackageNames: [
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection",
|
||||
"AWSSDK.SimpleEmail",
|
||||
"AWSSDK.SQS",
|
||||
"Handlebars.Net",
|
||||
"LaunchDarkly.ServerSdk",
|
||||
"MailKit",
|
||||
"Microsoft.AspNetCore.SignalR.Protocols.MessagePack",
|
||||
"Microsoft.AspNetCore.SignalR.StackExchangeRedis",
|
||||
"Microsoft.Azure.NotificationHubs",
|
||||
"Microsoft.Extensions.Configuration.EnvironmentVariables",
|
||||
"Microsoft.Extensions.Configuration.UserSecrets",
|
||||
"Microsoft.Extensions.Configuration",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions",
|
||||
"Microsoft.Extensions.DependencyInjection",
|
||||
"SendGrid",
|
||||
],
|
||||
description: "Tools owned dependencies",
|
||||
commitMessagePrefix: "[deps] Tools:",
|
||||
reviewers: ["team:team-tools-dev"],
|
||||
},
|
||||
{
|
||||
matchPackagePatterns: ["^Microsoft.AspNetCore.SignalR"],
|
||||
groupName: "SignalR",
|
||||
description: "Group SignalR to exclude them from the dotnet monorepo preset",
|
||||
},
|
||||
{
|
||||
matchPackagePatterns: ["^Microsoft.Extensions.Configuration"],
|
||||
groupName: "Microsoft.Extensions.Configuration",
|
||||
description: "Group Microsoft.Extensions.Configuration to exclude them from the dotnet monorepo preset",
|
||||
},
|
||||
{
|
||||
matchPackagePatterns: ["^Microsoft.Extensions.DependencyInjection"],
|
||||
groupName: "Microsoft.Extensions.DependencyInjection",
|
||||
description: "Group Microsoft.Extensions.DependencyInjection to exclude them from the dotnet monorepo preset",
|
||||
},
|
||||
{
|
||||
matchPackageNames: [
|
||||
"AngleSharp",
|
||||
|
11
.github/workflows/build.yml
vendored
11
.github/workflows/build.yml
vendored
@ -14,6 +14,7 @@ on:
|
||||
|
||||
env:
|
||||
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
||||
_GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@ -128,12 +129,18 @@ jobs:
|
||||
- name: Generate container image tag
|
||||
id: tag
|
||||
run: |
|
||||
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
|
||||
IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s#/#-#g")
|
||||
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" || "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then
|
||||
IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize branch name to alphanumeric only
|
||||
else
|
||||
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
||||
fi
|
||||
|
||||
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||
SANITIZED_REPO_NAME=$(echo "$_GITHUB_PR_REPO_NAME" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize repo name to alphanumeric only
|
||||
IMAGE_TAG=$SANITIZED_REPO_NAME-$IMAGE_TAG # Add repo name to the tag
|
||||
IMAGE_TAG=${IMAGE_TAG:0:128} # Limit to 128 characters, as that's the max length for Docker image tags
|
||||
fi
|
||||
|
||||
if [[ "$IMAGE_TAG" == "main" ]]; then
|
||||
IMAGE_TAG=dev
|
||||
fi
|
||||
|
15
.github/workflows/code-references.yml
vendored
15
.github/workflows/code-references.yml
vendored
@ -1,7 +1,10 @@
|
||||
name: Collect code references
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
on:
|
||||
push:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check-ld-secret:
|
||||
@ -37,12 +40,10 @@ jobs:
|
||||
|
||||
- name: 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:
|
||||
project-key: default
|
||||
environment-key: dev
|
||||
access-token: ${{ secrets.LD_ACCESS_TOKEN }}
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
accessToken: ${{ secrets.LD_ACCESS_TOKEN }}
|
||||
projKey: default
|
||||
|
||||
- name: Add label
|
||||
if: steps.collect.outputs.any-changed == 'true'
|
||||
|
@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2025.4.3</Version>
|
||||
<Version>2025.5.0</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
@ -129,6 +129,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "t
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.IntegrationTest", "test\Core.IntegrationTest\Core.IntegrationTest.csproj", "{3631BA42-6731-4118-A917-DAA43C5032B9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seeder", "util\Seeder\Seeder.csproj", "{9A612EBA-1C0E-42B8-982B-62F0EE81000A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -325,6 +329,14 @@ Global
|
||||
{3631BA42-6731-4118-A917-DAA43C5032B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -377,6 +389,8 @@ Global
|
||||
{4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||
{17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||
|
@ -8,7 +8,8 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
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.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
|
@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
@ -82,7 +83,7 @@ public class ProviderService : IProviderService
|
||||
_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);
|
||||
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.");
|
||||
}
|
||||
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;
|
||||
var subscription = await _providerBillingService.SetupSubscription(provider);
|
||||
provider.GatewaySubscriptionId = subscription.Id;
|
||||
|
@ -6,29 +6,39 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Braintree;
|
||||
using CsvHelper;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Stripe;
|
||||
|
||||
using static Bit.Core.Billing.Utilities;
|
||||
using Customer = Stripe.Customer;
|
||||
using Subscription = Stripe.Subscription;
|
||||
|
||||
namespace Bit.Commercial.Core.Billing;
|
||||
|
||||
public class ProviderBillingService(
|
||||
IBraintreeGateway braintreeGateway,
|
||||
IEventService eventService,
|
||||
IFeatureService featureService,
|
||||
IGlobalSettings globalSettings,
|
||||
@ -39,6 +49,7 @@ public class ProviderBillingService(
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IProviderPlanRepository providerPlanRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
ISetupIntentCache setupIntentCache,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ISubscriberService subscriberService,
|
||||
ITaxService taxService,
|
||||
@ -463,7 +474,8 @@ public class ProviderBillingService(
|
||||
|
||||
public async Task<Customer> SetupCustomer(
|
||||
Provider provider,
|
||||
TaxInfo taxInfo)
|
||||
TaxInfo taxInfo,
|
||||
TokenizedPaymentSource tokenizedPaymentSource = null)
|
||||
{
|
||||
if (taxInfo is not
|
||||
{
|
||||
@ -532,13 +544,97 @@ public class ProviderBillingService(
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
|
||||
CollectionMethod = usePaymentMethod ?
|
||||
StripeConstants.CollectionMethod.ChargeAutomatically : StripeConstants.CollectionMethod.SendInvoice,
|
||||
Customer = customer.Id,
|
||||
DaysUntilDue = 30,
|
||||
DaysUntilDue = usePaymentMethod ? null : 30,
|
||||
Items = subscriptionItemOptionsList,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "providerId", provider.Id.ToString() }
|
||||
},
|
||||
OffSession = true,
|
||||
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations
|
||||
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
|
||||
TrialPeriodDays = usePaymentMethod ? 14 : null
|
||||
};
|
||||
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
|
||||
@ -607,7 +723,10 @@ public class ProviderBillingService(
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
||||
|
||||
@ -11,36 +12,43 @@ public class MaxProjectsQuery : IMaxProjectsQuery
|
||||
{
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public MaxProjectsQuery(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IProjectRepository projectRepository)
|
||||
IProjectRepository projectRepository,
|
||||
IGlobalSettings globalSettings,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_projectRepository = projectRepository;
|
||||
_globalSettings = globalSettings;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
public async Task<(short? max, bool? overMax)> GetByOrgIdAsync(Guid organizationId, int projectsToAdd)
|
||||
{
|
||||
// "MaxProjects" only applies to free 2-person organizations, which can't be self-hosted.
|
||||
if (_globalSettings.SelfHosted)
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
if (org == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// TODO: PRICING -> https://bitwarden.atlassian.net/browse/PM-17122
|
||||
var plan = StaticStore.GetPlan(org.PlanType);
|
||||
if (plan?.SecretsManager == null)
|
||||
var plan = await _pricingClient.GetPlan(org.PlanType);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
var projects = await _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId);
|
||||
return ((short? max, bool? overMax))(projects + projectsToAdd > plan.SecretsManager.MaxProjects ? (plan.SecretsManager.MaxProjects, true) : (plan.SecretsManager.MaxProjects, false));
|
||||
}
|
||||
}
|
||||
|
@ -6,10 +6,10 @@ using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Commands;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -76,9 +76,8 @@ public class PostUserCommand(
|
||||
var invitedOrganizationUserId = result switch
|
||||
{
|
||||
Success<ScimInviteOrganizationUsersResponse> success => success.Value.InvitedUser.Id,
|
||||
Failure<ScimInviteOrganizationUsersResponse> failure when failure.Errors
|
||||
.Any(x => x.Message == NoUsersToInviteError.Code) => (Guid?)null,
|
||||
Failure<ScimInviteOrganizationUsersResponse> failure when failure.Errors.Length != 0 => throw MapToBitException(failure.Errors),
|
||||
Failure<ScimInviteOrganizationUsersResponse> { Error.Message: NoUsersToInviteError.Code } => (Guid?)null,
|
||||
Failure<ScimInviteOrganizationUsersResponse> failure => throw MapToBitException(failure.Error),
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
|
||||
|
@ -8,6 +8,7 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Bit.Commercial.Core.AdminConsole.Services;
|
||||
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
@ -7,6 +8,7 @@ using Bit.Core.AdminConsole.Models.Business.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
@ -38,7 +40,7 @@ public class ProviderServiceTests
|
||||
public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@ -50,12 +52,85 @@ public class ProviderServiceTests
|
||||
userService.GetUserByIdAsync(user.Id).Returns(user);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
[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,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
@ -75,7 +150,7 @@ public class ProviderServiceTests
|
||||
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
|
||||
|
||||
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" };
|
||||
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)}");
|
||||
|
||||
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>(
|
||||
p =>
|
||||
|
@ -2,18 +2,22 @@
|
||||
using System.Net;
|
||||
using Bit.Commercial.Core.Billing;
|
||||
using Bit.Commercial.Core.Billing.Models;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -24,11 +28,17 @@ using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Braintree;
|
||||
using CsvHelper;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
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;
|
||||
|
||||
@ -833,7 +843,7 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetupCustomer_Success(
|
||||
public async Task SetupCustomer_NoPaymentMethod_Success(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
Provider provider,
|
||||
TaxInfo taxInfo)
|
||||
@ -877,6 +887,301 @@ public class ProviderBillingServiceTests
|
||||
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]
|
||||
public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
@ -1044,7 +1349,7 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetupSubscription_Succeeds(
|
||||
public async Task SetupSubscription_SendInvoice_Succeeds(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
Provider provider)
|
||||
{
|
||||
@ -1127,6 +1432,303 @@ public class ProviderBillingServiceTests
|
||||
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
|
||||
|
||||
#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.Attributes;
|
||||
using Xunit;
|
||||
|
@ -1,9 +1,12 @@
|
||||
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@ -15,11 +18,26 @@ namespace Bit.Commercial.Core.Test.SecretsManager.Queries.Projects;
|
||||
[SutProviderCustomize]
|
||||
public class MaxProjectsQueryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetByOrgIdAsync_SelfHosted_ReturnsNulls(SutProvider<MaxProjectsQuery> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
|
||||
|
||||
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1);
|
||||
|
||||
Assert.Null(max);
|
||||
Assert.Null(overMax);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetByOrgIdAsync_OrganizationIsNull_ThrowsNotFound(SutProvider<MaxProjectsQuery> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(default).ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1));
|
||||
@ -28,26 +46,6 @@ public class MaxProjectsQueryTests
|
||||
.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]
|
||||
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||
[BitAutoData(PlanType.TeamsMonthly2020)]
|
||||
@ -65,9 +63,14 @@ public class MaxProjectsQueryTests
|
||||
public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType,
|
||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||
|
||||
organization.PlanType = planType;
|
||||
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);
|
||||
|
||||
Assert.Null(limit);
|
||||
@ -110,6 +113,9 @@ public class MaxProjectsQueryTests
|
||||
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(projects);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
|
||||
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);
|
||||
|
||||
Assert.NotNull(max);
|
||||
|
@ -124,8 +124,20 @@ services:
|
||||
profiles:
|
||||
- servicebus
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
container_name: bw-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
command: redis-server --appendonly yes
|
||||
profiles:
|
||||
- redis
|
||||
|
||||
volumes:
|
||||
mssql_dev_data:
|
||||
postgres_dev_data:
|
||||
mysql_dev_data:
|
||||
rabbitmq_data:
|
||||
redis_data:
|
||||
|
@ -4,6 +4,7 @@
|
||||
"rollForward": "latestFeature"
|
||||
},
|
||||
"msbuild-sdks": {
|
||||
"Microsoft.Build.Traversal": "4.1.0"
|
||||
"Microsoft.Build.Traversal": "4.1.0",
|
||||
"Microsoft.Build.Sql": "0.1.9-preview"
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ using Bit.Admin.Enums;
|
||||
using Bit.Admin.Models;
|
||||
using Bit.Admin.Services;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -89,7 +88,7 @@ public class UsersController : Controller
|
||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
|
||||
|
||||
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
|
||||
var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id);
|
||||
return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain));
|
||||
}
|
||||
|
||||
@ -106,7 +105,7 @@ public class UsersController : Controller
|
||||
var billingInfo = await _paymentService.GetBillingAsync(user);
|
||||
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
|
||||
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
|
||||
var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id);
|
||||
var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id);
|
||||
|
||||
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired));
|
||||
@ -167,7 +166,6 @@ public class UsersController : Controller
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.User_NewDeviceException_Edit)]
|
||||
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
|
||||
public async Task<IActionResult> ToggleNewDeviceVerification(Guid id)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(id);
|
||||
@ -179,12 +177,4 @@ public class UsersController : Controller
|
||||
await _userService.ToggleNewDeviceVerificationException(user.Id);
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
|
||||
// TODO: Feature flag to be removed in PM-14207
|
||||
private async Task<bool?> AccountDeprovisioningEnabled(Guid userId)
|
||||
{
|
||||
return _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
? await _userService.IsClaimedByAnyOrganizationAsync(userId)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ namespace Bit.Api.AdminConsole.Authorization;
|
||||
public static class HttpContextExtensions
|
||||
{
|
||||
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>
|
||||
/// 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>
|
||||
/// 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>
|
||||
/// <param name="httpContext"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public static Guid GetOrganizationId(this HttpContext httpContext)
|
||||
{
|
||||
httpContext.GetRouteData().Values.TryGetValue("orgId", out var orgIdParam);
|
||||
if (orgIdParam == null || !Guid.TryParse(orgIdParam.ToString(), out var orgId))
|
||||
var routeValues = httpContext.GetRouteData().Values;
|
||||
|
||||
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.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Response;
|
||||
@ -63,6 +64,7 @@ public class OrganizationUsersController : Controller
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
|
||||
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||
private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand;
|
||||
|
||||
public OrganizationUsersController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -89,7 +91,8 @@ public class OrganizationUsersController : Controller
|
||||
IFeatureService featureService,
|
||||
IPricingClient pricingClient,
|
||||
IConfirmOrganizationUserCommand confirmOrganizationUserCommand,
|
||||
IRestoreOrganizationUserCommand restoreOrganizationUserCommand)
|
||||
IRestoreOrganizationUserCommand restoreOrganizationUserCommand,
|
||||
IInitPendingOrganizationCommand initPendingOrganizationCommand)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -116,6 +119,7 @@ public class OrganizationUsersController : Controller
|
||||
_pricingClient = pricingClient;
|
||||
_confirmOrganizationUserCommand = confirmOrganizationUserCommand;
|
||||
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||
_initPendingOrganizationCommand = initPendingOrganizationCommand;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -159,6 +163,12 @@ public class OrganizationUsersController : Controller
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false)
|
||||
{
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.SeparateCustomRolePermissions))
|
||||
{
|
||||
return await GetvNextAsync(orgId, includeGroups, includeCollections);
|
||||
}
|
||||
|
||||
var authorized = (await _authorizationService.AuthorizeAsync(
|
||||
User, new OrganizationScope(orgId), OrganizationUserUserDetailsOperations.ReadAll)).Succeeded;
|
||||
if (!authorized)
|
||||
@ -188,6 +198,37 @@ public class OrganizationUsersController : Controller
|
||||
return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(responses);
|
||||
}
|
||||
|
||||
private async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> GetvNextAsync(Guid orgId, bool includeGroups = false, bool includeCollections = false)
|
||||
{
|
||||
var request = new OrganizationUserUserDetailsQueryRequest
|
||||
{
|
||||
OrganizationId = orgId,
|
||||
IncludeGroups = includeGroups,
|
||||
IncludeCollections = includeCollections,
|
||||
};
|
||||
|
||||
if ((await _authorizationService.AuthorizeAsync(User, new ManageUsersRequirement())).Succeeded)
|
||||
{
|
||||
return GetResultListResponseModel(await _organizationUserUserDetailsQuery.Get(request));
|
||||
}
|
||||
|
||||
if ((await _authorizationService.AuthorizeAsync(User, new ManageAccountRecoveryRequirement())).Succeeded)
|
||||
{
|
||||
return GetResultListResponseModel(await _organizationUserUserDetailsQuery.GetAccountRecoveryEnrolledUsers(request));
|
||||
}
|
||||
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
private ListResponseModel<OrganizationUserUserDetailsResponseModel> GetResultListResponseModel(IEnumerable<(OrganizationUserUserDetails OrgUser,
|
||||
bool TwoFactorEnabled, bool ClaimedByOrganization)> results)
|
||||
{
|
||||
return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(results
|
||||
.Select(result => new OrganizationUserUserDetailsResponseModel(result))
|
||||
.ToList());
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("{id}/groups")]
|
||||
public async Task<IEnumerable<string>> GetGroups(string orgId, string id)
|
||||
{
|
||||
@ -313,7 +354,7 @@ public class OrganizationUsersController : Controller
|
||||
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 _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id);
|
||||
}
|
||||
@ -575,7 +616,6 @@ public class OrganizationUsersController : Controller
|
||||
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
|
||||
}
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
||||
[HttpDelete("{id}/delete-account")]
|
||||
[HttpPost("{id}/delete-account")]
|
||||
public async Task DeleteAccount(Guid orgId, Guid id)
|
||||
@ -594,7 +634,6 @@ public class OrganizationUsersController : Controller
|
||||
await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
|
||||
}
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
||||
[HttpDelete("delete-account")]
|
||||
[HttpPost("delete-account")]
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||
@ -719,11 +758,6 @@ public class OrganizationUsersController : Controller
|
||||
|
||||
private async Task<IDictionary<Guid, bool>> GetClaimedByOrganizationStatusAsync(Guid orgId, IEnumerable<Guid> userIds)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
return userIds.ToDictionary(kvp => kvp, kvp => false);
|
||||
}
|
||||
|
||||
var usersOrganizationClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgId, userIds);
|
||||
return usersOrganizationClaimedStatus;
|
||||
}
|
||||
|
@ -279,8 +279,7 @@ public class OrganizationsController : Controller
|
||||
throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving.");
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& (await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id))
|
||||
if ((await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id))
|
||||
{
|
||||
throw new BadRequestException("Claimed user account cannot leave claiming organization. Contact your organization administrator for additional details.");
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
using Bit.Api.AdminConsole.Models.Response.Helpers;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
@ -79,7 +78,7 @@ public class PoliciesController : Controller
|
||||
return new PolicyDetailResponseModel(new Policy { Type = (PolicyType)type });
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && policy.Type is PolicyType.SingleOrg)
|
||||
if (policy.Type is PolicyType.SingleOrg)
|
||||
{
|
||||
return await policy.GetSingleOrgPolicyDetailResponseAsync(_organizationHasVerifiedDomainsQuery);
|
||||
}
|
||||
|
@ -84,22 +84,22 @@ public class ProvidersController : Controller
|
||||
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
|
||||
var taxInfo = model.TaxInfo != null
|
||||
? new TaxInfo
|
||||
{
|
||||
BillingAddressCountry = model.TaxInfo.Country,
|
||||
BillingAddressPostalCode = model.TaxInfo.PostalCode,
|
||||
TaxIdNumber = model.TaxInfo.TaxId,
|
||||
BillingAddressLine1 = model.TaxInfo.Line1,
|
||||
BillingAddressLine2 = model.TaxInfo.Line2,
|
||||
BillingAddressCity = model.TaxInfo.City,
|
||||
BillingAddressState = model.TaxInfo.State
|
||||
}
|
||||
: null;
|
||||
var taxInfo = new TaxInfo
|
||||
{
|
||||
BillingAddressCountry = model.TaxInfo.Country,
|
||||
BillingAddressPostalCode = model.TaxInfo.PostalCode,
|
||||
TaxIdNumber = model.TaxInfo.TaxId,
|
||||
BillingAddressLine1 = model.TaxInfo.Line1,
|
||||
BillingAddressLine2 = model.TaxInfo.Line2,
|
||||
BillingAddressCity = model.TaxInfo.City,
|
||||
BillingAddressState = model.TaxInfo.State
|
||||
};
|
||||
|
||||
var tokenizedPaymentSource = model.PaymentSource?.ToDomain();
|
||||
|
||||
var response =
|
||||
await _providerService.CompleteSetupAsync(model.ToProvider(provider), userId, model.Token, model.Key,
|
||||
taxInfo);
|
||||
taxInfo, tokenizedPaymentSource);
|
||||
|
||||
return new ProviderResponseModel(response);
|
||||
}
|
||||
|
@ -75,6 +75,8 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
||||
|
||||
public string InitiationPath { get; set; }
|
||||
|
||||
public bool SkipTrial { get; set; }
|
||||
|
||||
public virtual OrganizationSignup ToOrganizationSignup(User user)
|
||||
{
|
||||
var orgSignup = new OrganizationSignup
|
||||
@ -107,6 +109,7 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
||||
BillingAddressCountry = BillingAddressCountry,
|
||||
},
|
||||
InitiationPath = InitiationPath,
|
||||
SkipTrial = SkipTrial
|
||||
};
|
||||
|
||||
Keys?.ToOrganizationSignup(orgSignup);
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Utilities;
|
||||
@ -23,7 +24,9 @@ public class ProviderSetupRequestModel
|
||||
public string Token { get; set; }
|
||||
[Required]
|
||||
public string Key { get; set; }
|
||||
[Required]
|
||||
public ExpandedTaxInfoUpdateRequestModel TaxInfo { get; set; }
|
||||
public TokenizedPaymentSourceRequestBody PaymentSource { get; set; }
|
||||
|
||||
public virtual Provider ToProvider(Provider provider)
|
||||
{
|
||||
|
@ -126,6 +126,26 @@ public class OrganizationUserUserMiniDetailsResponseModel : ResponseModel
|
||||
|
||||
public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel
|
||||
{
|
||||
public OrganizationUserUserDetailsResponseModel((OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization) data, string obj = "organizationUserUserDetails")
|
||||
: base(data.OrgUser, obj)
|
||||
{
|
||||
if (data.OrgUser == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(data.OrgUser));
|
||||
}
|
||||
|
||||
Name = data.OrgUser.Name;
|
||||
Email = data.OrgUser.Email;
|
||||
AvatarColor = data.OrgUser.AvatarColor;
|
||||
TwoFactorEnabled = data.TwoFactorEnabled;
|
||||
SsoBound = !string.IsNullOrWhiteSpace(data.OrgUser.SsoExternalId);
|
||||
Collections = data.OrgUser.Collections.Select(c => new SelectionReadOnlyResponseModel(c));
|
||||
Groups = data.OrgUser.Groups;
|
||||
// Prevent reset password when using key connector.
|
||||
ResetPasswordEnrolled = ResetPasswordEnrolled && !data.OrgUser.UsesKeyConnector;
|
||||
ClaimedByOrganization = data.ClaimedByOrganization;
|
||||
}
|
||||
|
||||
public OrganizationUserUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,
|
||||
bool twoFactorEnabled, bool claimedByOrganization, string obj = "organizationUserUserDetails")
|
||||
: base(organizationUser, obj)
|
||||
|
@ -58,7 +58,8 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
ProviderName = organization.ProviderName;
|
||||
ProviderType = organization.ProviderType;
|
||||
FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName;
|
||||
FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null &&
|
||||
IsAdminInitiated = organization.IsAdminInitiated ?? false;
|
||||
FamilySponsorshipAvailable = (FamilySponsorshipFriendlyName == null || IsAdminInitiated) &&
|
||||
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
|
||||
.UsersCanSponsor(organization);
|
||||
ProductTierType = organization.PlanType.GetProductTier();
|
||||
@ -135,7 +136,6 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
/// <summary>
|
||||
/// Obsolete.
|
||||
///
|
||||
/// See <see cref="UserIsClaimedByOrganization"/>
|
||||
/// </summary>
|
||||
[Obsolete("Please use UserIsClaimedByOrganization instead. This property will be removed in a future version.")]
|
||||
@ -145,16 +145,14 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
set => UserIsClaimedByOrganization = value;
|
||||
}
|
||||
/// <summary>
|
||||
/// Indicates if the organization claims the user.
|
||||
/// Indicates if the user is claimed by the organization.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An organization claims a user if the user's email domain is verified by the organization and the user is a member of it.
|
||||
/// A user is claimed by an organization if the user's email domain is verified by the organization and the user is a member.
|
||||
/// The organization must be enabled and able to have verified domains.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// False if the Account Deprovisioning feature flag is disabled.
|
||||
/// </returns>
|
||||
public bool UserIsClaimedByOrganization { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public bool IsAdminInitiated { get; set; }
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ public class MembersController : Controller
|
||||
{
|
||||
return new NotFoundResult();
|
||||
}
|
||||
var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser),
|
||||
var response = new MemberResponseModel(orgUser, await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUser),
|
||||
collections);
|
||||
return new JsonResult(response);
|
||||
}
|
||||
@ -185,7 +185,7 @@ public class MembersController : Controller
|
||||
{
|
||||
var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
||||
response = new MemberResponseModel(existingUserDetails,
|
||||
await _userService.TwoFactorIsEnabledAsync(existingUserDetails), associations);
|
||||
await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(existingUserDetails), associations);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -4,8 +4,6 @@
|
||||
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||
<ANCMPreConfiguredForIIS>true</ANCMPreConfiguredForIIS>
|
||||
<!-- Temp exclusions until warnings are fixed -->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8604</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
@ -16,6 +16,7 @@ using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -45,6 +46,7 @@ public class AccountsController : Controller
|
||||
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
|
||||
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
||||
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
|
||||
@ -68,6 +70,7 @@ public class AccountsController : Controller
|
||||
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
|
||||
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
|
||||
IRotateUserKeyCommand rotateUserKeyCommand,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IFeatureService featureService,
|
||||
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
|
||||
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
|
||||
@ -87,6 +90,7 @@ public class AccountsController : Controller
|
||||
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
|
||||
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
|
||||
_rotateUserKeyCommand = rotateUserKeyCommand;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_featureService = featureService;
|
||||
_cipherValidator = cipherValidator;
|
||||
_folderValidator = folderValidator;
|
||||
@ -389,7 +393,7 @@ public class AccountsController : Controller
|
||||
await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,
|
||||
ProviderUserStatusType.Confirmed);
|
||||
|
||||
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
||||
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
|
||||
@ -423,7 +427,7 @@ public class AccountsController : Controller
|
||||
|
||||
await _userService.SaveUserAsync(model.ToUser(user));
|
||||
|
||||
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
||||
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
|
||||
@ -442,7 +446,7 @@ public class AccountsController : Controller
|
||||
}
|
||||
await _userService.SaveUserAsync(model.ToUser(user), true);
|
||||
|
||||
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
||||
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
|
||||
@ -514,9 +518,8 @@ public class AccountsController : Controller
|
||||
}
|
||||
else
|
||||
{
|
||||
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization.
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||
// Check if the user is claimed by any organization.
|
||||
if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||
{
|
||||
throw new BadRequestException("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.");
|
||||
}
|
||||
@ -693,7 +696,6 @@ public class AccountsController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
|
||||
[AllowAnonymous]
|
||||
[HttpPost("resend-new-device-otp")]
|
||||
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#nullable enable
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Core.Billing.Models.Api.Requests.Accounts;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -3,6 +3,7 @@ using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Request.Accounts;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
@ -22,7 +23,8 @@ namespace Bit.Api.Billing.Controllers;
|
||||
[Route("accounts")]
|
||||
[Authorize("Application")]
|
||||
public class AccountsController(
|
||||
IUserService userService) : Controller
|
||||
IUserService userService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller
|
||||
{
|
||||
[HttpPost("premium")]
|
||||
public async Task<PaymentResponseModel> PostPremiumAsync(
|
||||
@ -56,7 +58,7 @@ public class AccountsController(
|
||||
model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license,
|
||||
new TaxInfo { BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode });
|
||||
|
||||
var userTwoFactorEnabled = await userService.TwoFactorIsEnabledAsync(user);
|
||||
var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
|
||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Models.Api.Requests.Organizations;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
@ -1,12 +1,15 @@
|
||||
#nullable enable
|
||||
using System.Diagnostics;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Api.Billing.Queries.Organizations;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -24,6 +27,7 @@ public class OrganizationBillingController(
|
||||
IFeatureService featureService,
|
||||
IOrganizationBillingService organizationBillingService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationWarningsQuery organizationWarningsQuery,
|
||||
IPaymentService paymentService,
|
||||
IPricingClient pricingClient,
|
||||
ISubscriberService subscriberService,
|
||||
@ -290,6 +294,7 @@ public class OrganizationBillingController(
|
||||
sale.SubscriptionSetup.SkipTrial = true;
|
||||
await organizationBillingService.Finalize(sale);
|
||||
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)
|
||||
{
|
||||
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);
|
||||
@ -335,4 +340,28 @@ public class OrganizationBillingController(
|
||||
|
||||
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.Response;
|
||||
using Bit.Api.Models.Response.Organizations;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
|
||||
@ -8,6 +9,7 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Api.Request.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.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -105,13 +107,16 @@ public class OrganizationSponsorshipsController : Controller
|
||||
model.FriendlyName,
|
||||
model.IsAdminInitiated.GetValueOrDefault(),
|
||||
model.Notes);
|
||||
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name);
|
||||
if (sponsorship.OfferedToEmail != null)
|
||||
{
|
||||
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize("Application")]
|
||||
[HttpPost("{sponsoringOrgId}/families-for-enterprise/resend")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task ResendSponsorshipOffer(Guid sponsoringOrgId)
|
||||
public async Task ResendSponsorshipOffer(Guid sponsoringOrgId, [FromQuery] string sponsoredFriendlyName)
|
||||
{
|
||||
var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsoringOrgId,
|
||||
PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||
@ -124,11 +129,14 @@ public class OrganizationSponsorshipsController : Controller
|
||||
var sponsoringOrgUser = await _organizationUserRepository
|
||||
.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default);
|
||||
|
||||
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(
|
||||
await _organizationRepository.GetByIdAsync(sponsoringOrgId),
|
||||
sponsoringOrgUser,
|
||||
await _organizationSponsorshipRepository
|
||||
.GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id));
|
||||
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);
|
||||
var filteredSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName != null && s.FriendlyName.Equals(sponsoredFriendlyName, StringComparison.OrdinalIgnoreCase));
|
||||
if (filteredSponsorship != null)
|
||||
{
|
||||
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(
|
||||
await _organizationRepository.GetByIdAsync(sponsoringOrgId),
|
||||
sponsoringOrgUser, filteredSponsorship);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize("Application")]
|
||||
@ -199,7 +207,7 @@ public class OrganizationSponsorshipsController : Controller
|
||||
[HttpDelete("{sponsoringOrganizationId}")]
|
||||
[HttpPost("{sponsoringOrganizationId}/delete")]
|
||||
[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);
|
||||
@ -209,7 +217,7 @@ public class OrganizationSponsorshipsController : Controller
|
||||
}
|
||||
|
||||
var existingOrgSponsorship = await _organizationSponsorshipRepository
|
||||
.GetBySponsoringOrganizationUserIdAsync(orgUser.Id);
|
||||
.GetBySponsoringOrganizationUserIdAsync(orgUser.Id, isAdminInitiated);
|
||||
|
||||
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
|
||||
}
|
||||
@ -246,5 +254,30 @@ public class OrganizationSponsorshipsController : Controller
|
||||
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);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Models.BitStripe;
|
||||
using Bit.Core.Services;
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
|
36
src/Api/Billing/Controllers/TaxController.cs
Normal file
36
src/Api/Billing/Controllers/TaxController.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Core.Billing.Tax.Commands;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Billing.Controllers;
|
||||
|
||||
[Authorize("Application")]
|
||||
[Route("tax")]
|
||||
public class TaxController(
|
||||
IPreviewTaxAmountCommand previewTaxAmountCommand) : BaseBillingController
|
||||
{
|
||||
[HttpPost("preview-amount/organization-trial")]
|
||||
public async Task<IResult> PreviewTaxAmountForOrganizationTrialAsync(
|
||||
[FromBody] PreviewTaxAmountForOrganizationTrialRequestBody requestBody)
|
||||
{
|
||||
var parameters = new OrganizationTrialParameters
|
||||
{
|
||||
PlanType = requestBody.PlanType,
|
||||
ProductType = requestBody.ProductType,
|
||||
TaxInformation = new OrganizationTrialParameters.TaxInformationDTO
|
||||
{
|
||||
Country = requestBody.TaxInformation.Country,
|
||||
PostalCode = requestBody.TaxInformation.PostalCode,
|
||||
TaxId = requestBody.TaxInformation.TaxId
|
||||
}
|
||||
};
|
||||
|
||||
var result = await previewTaxAmountCommand.Run(parameters);
|
||||
|
||||
return result.Match<IResult>(
|
||||
taxAmount => TypedResults.Ok(new { TaxAmount = taxAmount }),
|
||||
badRequest => Error.BadRequest(badRequest.TranslationKey),
|
||||
unhandled => Error.ServerError(unhandled.TranslationKey));
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
#nullable enable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Billing.Enums;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests;
|
||||
|
||||
public class PreviewTaxAmountForOrganizationTrialRequestBody
|
||||
{
|
||||
[Required]
|
||||
public PlanType PlanType { get; set; }
|
||||
|
||||
[Required]
|
||||
public ProductType ProductType { get; set; }
|
||||
|
||||
[Required] public TaxInformationDTO TaxInformation { get; set; } = null!;
|
||||
|
||||
public class TaxInformationDTO
|
||||
{
|
||||
[Required]
|
||||
public string Country { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
public string PostalCode { get; set; } = null!;
|
||||
|
||||
public string? TaxId { get; set; }
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests;
|
||||
|
||||
|
@ -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.Tax.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Stripe;
|
||||
|
||||
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;
|
||||
|
||||
|
@ -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 Guid? UserGuid { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string Email { get; set; }
|
||||
public bool UsesKeyConnector { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A distinct list of the cipher ids associated with
|
||||
/// A distinct list of the cipher ids associated with
|
||||
/// the organization member
|
||||
/// </summary>
|
||||
public IEnumerable<string> CipherIds { get; set; }
|
||||
|
||||
public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails)
|
||||
{
|
||||
this.UserGuid = memberAccessCipherDetails.UserGuid;
|
||||
this.UserName = memberAccessCipherDetails.UserName;
|
||||
this.Email = memberAccessCipherDetails.Email;
|
||||
this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector;
|
@ -58,6 +58,13 @@ public class JobsHostedService : BaseJobsHostedService
|
||||
.StartNow()
|
||||
.WithCronSchedule("0 0 * * * ?")
|
||||
.Build();
|
||||
var updatePhishingDomainsTrigger = TriggerBuilder.Create()
|
||||
.WithIdentity("UpdatePhishingDomainsTrigger")
|
||||
.StartNow()
|
||||
.WithSimpleSchedule(x => x
|
||||
.WithIntervalInHours(24)
|
||||
.RepeatForever())
|
||||
.Build();
|
||||
|
||||
|
||||
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(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationDomainJob), validateOrganizationDomainTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(UpdatePhishingDomainsJob), updatePhishingDomainsTrigger),
|
||||
};
|
||||
|
||||
if (_globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication)
|
||||
@ -96,6 +104,7 @@ public class JobsHostedService : BaseJobsHostedService
|
||||
services.AddTransient<ValidateUsersJob>();
|
||||
services.AddTransient<ValidateOrganizationsJob>();
|
||||
services.AddTransient<ValidateOrganizationDomainJob>();
|
||||
services.AddTransient<UpdatePhishingDomainsJob>();
|
||||
}
|
||||
|
||||
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.Vault.Entities;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Api.Billing;
|
||||
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
@ -182,6 +183,9 @@ public class Startup
|
||||
services.AddBillingOperations();
|
||||
services.AddReportingServices();
|
||||
services.AddImportServices();
|
||||
services.AddPhishingDomainServices(globalSettings);
|
||||
|
||||
services.AddBillingQueries();
|
||||
|
||||
// Authorization Handlers
|
||||
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.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
||||
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.Utilities;
|
||||
using Bit.Core.Vault.Authorization.SecurityTasks;
|
||||
@ -109,4 +113,25 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization.
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||
// Check if the user is claimed by any organization.
|
||||
if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||
{
|
||||
throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details.");
|
||||
}
|
||||
@ -1241,6 +1240,20 @@ public class CiphersController : Controller
|
||||
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}")]
|
||||
public async Task<AttachmentResponseModel> GetAttachmentData(Guid id, string attachmentId)
|
||||
{
|
||||
@ -1287,18 +1300,17 @@ public class CiphersController : Controller
|
||||
|
||||
[HttpDelete("{id}/attachment/{attachmentId}/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 cipher = await _cipherRepository.GetByIdAsync(idGuid);
|
||||
var cipher = await _cipherRepository.GetByIdAsync(id);
|
||||
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
||||
!await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, true);
|
||||
return await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, true);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
|
@ -3,6 +3,7 @@ using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -37,6 +38,7 @@ public class SyncController : Controller
|
||||
private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion);
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
|
||||
public SyncController(
|
||||
IUserService userService,
|
||||
@ -51,7 +53,8 @@ public class SyncController : Controller
|
||||
GlobalSettings globalSettings,
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
IApplicationCacheService applicationCacheService)
|
||||
IApplicationCacheService applicationCacheService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
||||
{
|
||||
_userService = userService;
|
||||
_folderRepository = folderRepository;
|
||||
@ -66,6 +69,7 @@ public class SyncController : Controller
|
||||
_currentContext = currentContext;
|
||||
_featureService = featureService;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@ -102,7 +106,7 @@ public class SyncController : Controller
|
||||
collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
|
||||
}
|
||||
|
||||
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
||||
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
||||
var organizationClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(user.Id);
|
||||
var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id);
|
||||
|
@ -37,6 +37,10 @@
|
||||
},
|
||||
"storage": {
|
||||
"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",
|
||||
"region": "SECRET"
|
||||
},
|
||||
"phishingDomain": {
|
||||
"updateUrl": "SECRET"
|
||||
},
|
||||
"distributedIpRateLimiting": {
|
||||
"enabled": true,
|
||||
"maxRedisTimeoutsThreshold": 10,
|
||||
|
@ -63,6 +63,12 @@ public class FreshdeskController : Controller
|
||||
note += $"<li>Region: {_billingSettings.FreshDesk.Region}</li>";
|
||||
var customFields = new Dictionary<string, object>();
|
||||
var user = await _userRepository.GetByEmailAsync(ticketContactEmail);
|
||||
if (user == null)
|
||||
{
|
||||
note += $"<li>No user found: {ticketContactEmail}</li>";
|
||||
await CreateNote(ticketId, note);
|
||||
}
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
var userLink = $"{_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}";
|
||||
@ -121,18 +127,7 @@ public class FreshdeskController : Controller
|
||||
Content = JsonContent.Create(updateBody),
|
||||
};
|
||||
await CallFreshdeskApiAsync(updateRequest);
|
||||
|
||||
var noteBody = new Dictionary<string, object>
|
||||
{
|
||||
{ "body", $"<ul>{note}</ul>" },
|
||||
{ "private", true }
|
||||
};
|
||||
var noteRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId))
|
||||
{
|
||||
Content = JsonContent.Create(noteBody),
|
||||
};
|
||||
await CallFreshdeskApiAsync(noteRequest);
|
||||
await CreateNote(ticketId, note);
|
||||
}
|
||||
|
||||
return new OkResult();
|
||||
@ -208,6 +203,21 @@ public class FreshdeskController : Controller
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task CreateNote(string ticketId, string note)
|
||||
{
|
||||
var noteBody = new Dictionary<string, object>
|
||||
{
|
||||
{ "body", $"<ul>{note}</ul>" },
|
||||
{ "private", true }
|
||||
};
|
||||
var noteRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId))
|
||||
{
|
||||
Content = JsonContent.Create(noteBody),
|
||||
};
|
||||
await CallFreshdeskApiAsync(noteRequest);
|
||||
}
|
||||
|
||||
private async Task AddAnswerNoteToTicketAsync(string note, string ticketId)
|
||||
{
|
||||
// if there is no content, then we don't need to add a note
|
||||
|
@ -4,8 +4,8 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
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 UseRiskInsights { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public bool? IsAdminInitiated { get; set; }
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ public class VerifyOrganizationDomainCommand(
|
||||
IDnsResolverService dnsResolverService,
|
||||
IEventService eventService,
|
||||
IGlobalSettings globalSettings,
|
||||
IFeatureService featureService,
|
||||
ICurrentContext currentContext,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IMailService mailService,
|
||||
@ -125,11 +124,8 @@ public class VerifyOrganizationDomainCommand(
|
||||
|
||||
private async Task DomainVerificationSideEffectsAsync(OrganizationDomain domain, IActingUser actingUser)
|
||||
{
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser);
|
||||
await SendVerifiedDomainUserEmailAsync(domain);
|
||||
}
|
||||
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser);
|
||||
await SendVerifiedDomainUserEmailAsync(domain);
|
||||
}
|
||||
|
||||
private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser) =>
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -24,6 +25,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||
|
||||
public AcceptOrgUserCommand(
|
||||
@ -34,6 +36,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
||||
IPolicyService policyService,
|
||||
IMailService mailService,
|
||||
IUserRepository userRepository,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
|
||||
{
|
||||
|
||||
@ -45,6 +48,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
||||
_policyService = policyService;
|
||||
_mailService = mailService;
|
||||
_userRepository = userRepository;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||
}
|
||||
|
||||
@ -192,7 +196,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
||||
}
|
||||
|
||||
// Enforce Two Factor Authentication Policy of organization user is trying to join
|
||||
if (!await userService.TwoFactorIsEnabledAsync(user))
|
||||
if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
|
||||
{
|
||||
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
|
||||
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
|
||||
|
@ -6,4 +6,8 @@ namespace Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
public interface IOrganizationUserUserDetailsQuery
|
||||
{
|
||||
Task<IEnumerable<OrganizationUserUserDetails>> GetOrganizationUserUserDetails(OrganizationUserUserDetailsQueryRequest request);
|
||||
|
||||
Task<IEnumerable<(OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization)>> Get(OrganizationUserUserDetailsQueryRequest request);
|
||||
|
||||
Task<IEnumerable<(OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization)>> GetAccountRecoveryEnrolledUsers(OrganizationUserUserDetailsQueryRequest request);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
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;
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
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;
|
||||
|
||||
|
@ -1,17 +1,17 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
using Bit.Core.AdminConsole.Interfaces;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
|
||||
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.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Commands;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -50,11 +50,11 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
||||
{
|
||||
case Failure<InviteOrganizationUsersResponse> failure:
|
||||
return new Failure<ScimInviteOrganizationUsersResponse>(
|
||||
failure.Errors.Select(error => new Error<ScimInviteOrganizationUsersResponse>(error.Message,
|
||||
new Error<ScimInviteOrganizationUsersResponse>(failure.Error.Message,
|
||||
new ScimInviteOrganizationUsersResponse
|
||||
{
|
||||
InvitedUser = error.ErroredValue.InvitedUsers.FirstOrDefault()
|
||||
})));
|
||||
InvitedUser = failure.Error.ErroredValue.InvitedUsers.FirstOrDefault()
|
||||
}));
|
||||
|
||||
case Success<InviteOrganizationUsersResponse> success when success.Value.InvitedUsers.Any():
|
||||
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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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.Shared.Validation;
|
||||
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
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;
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
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;
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||
|
||||
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.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Shared.Validation;
|
||||
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
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;
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
|
||||
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.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;
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Shared.Validation;
|
||||
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
|
||||
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.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
@ -9,12 +13,21 @@ namespace Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuery
|
||||
{
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IGetOrganizationUsersClaimedStatusQuery _getOrganizationUsersClaimedStatusQuery;
|
||||
|
||||
public OrganizationUserUserDetailsQuery(
|
||||
IOrganizationUserRepository organizationUserRepository
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IFeatureService featureService,
|
||||
IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery
|
||||
)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_featureService = featureService;
|
||||
_getOrganizationUsersClaimedStatusQuery = getOrganizationUsersClaimedStatusQuery;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -37,4 +50,42 @@ public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuer
|
||||
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);
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null)
|
||||
if (deletingUserId.HasValue && eventSystemUser == null)
|
||||
{
|
||||
var claimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgUser.OrganizationId, new[] { orgUser.Id });
|
||||
if (claimedStatus.TryGetValue(orgUser.Id, out var isClaimed) && isClaimed)
|
||||
@ -214,7 +214,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
|
||||
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))
|
||||
: filteredUsers.ToDictionary(u => u.Id, u => false);
|
||||
var result = new List<(OrganizationUser OrganizationUser, string ErrorMessage)>();
|
||||
|
@ -1,8 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Commands;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
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 (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
var currentUser = _currentContext.UserId ?? Guid.Empty;
|
||||
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
|
||||
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
||||
}
|
||||
else
|
||||
{
|
||||
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
|
||||
}
|
||||
var currentUser = _currentContext.UserId ?? Guid.Empty;
|
||||
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
|
||||
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,42 +109,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
_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)
|
||||
{
|
||||
if (policyUpdate is not { Enabled: true })
|
||||
@ -165,8 +122,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
return validateDecryptionErrorMessage;
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))
|
||||
if (await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))
|
||||
{
|
||||
return ClaimedDomainSingleOrganizationRequiredErrorMessage;
|
||||
}
|
||||
|
@ -23,8 +23,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
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.";
|
||||
@ -38,8 +36,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
IOrganizationRepository organizationRepository,
|
||||
ICurrentContext currentContext,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IFeatureService featureService,
|
||||
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -47,8 +43,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
_organizationRepository = organizationRepository;
|
||||
_currentContext = currentContext;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_featureService = featureService;
|
||||
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
||||
}
|
||||
|
||||
@ -56,16 +50,9 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
{
|
||||
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);
|
||||
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
||||
}
|
||||
else
|
||||
{
|
||||
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
|
||||
}
|
||||
var currentUser = _currentContext.UserId ?? Guid.Empty;
|
||||
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
|
||||
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,40 +108,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
_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(
|
||||
IEnumerable<OrganizationUserUserDetails> orgUserDetails,
|
||||
IEnumerable<(OrganizationUserUserDetails user, bool isTwoFactorEnabled)> organizationUsersTwoFactorEnabled) =>
|
||||
|
@ -48,14 +48,8 @@ public interface IOrganizationService
|
||||
Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid organizationId,
|
||||
IEnumerable<Guid> organizationUserIds, Guid? revokingUserId);
|
||||
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<(bool canScale, string failureReason)> CanScaleAsync(Organization organization, int seatsToAdd);
|
||||
|
||||
void ValidatePasswordManagerPlan(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.Models.Business.Provider;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
@ -7,7 +8,8 @@ namespace Bit.Core.AdminConsole.Services;
|
||||
|
||||
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<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