mirror of
https://github.com/bitwarden/server
synced 2025-12-27 21:53:24 +00:00
Merge branch 'main' of github.com:bitwarden/server into arch/seeder-api
# Conflicts: # util/Seeder/Recipes/OrganizationWithUsersRecipe.cs
This commit is contained in:
11
.github/ISSUE_TEMPLATE/bw-lite.yml
vendored
11
.github/ISSUE_TEMPLATE/bw-lite.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Bitwarden Lite Deployment Bug Report
|
||||
name: Bitwarden lite Deployment Bug Report
|
||||
description: File a bug report
|
||||
labels: [bug, bw-lite-deploy]
|
||||
body:
|
||||
@@ -70,15 +70,6 @@ body:
|
||||
mariadb:10
|
||||
# Postgres Example
|
||||
postgres:14
|
||||
- type: textarea
|
||||
id: epic-label
|
||||
attributes:
|
||||
label: Issue-Link
|
||||
description: Link to our pinned issue, tracking all Bitwarden Lite
|
||||
value: |
|
||||
https://github.com/bitwarden/server/issues/2480
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: issue-tracking-info
|
||||
attributes:
|
||||
|
||||
3
.github/renovate.json5
vendored
3
.github/renovate.json5
vendored
@@ -44,6 +44,7 @@
|
||||
{
|
||||
matchPackageNames: ["https://github.com/bitwarden/sdk-internal.git"],
|
||||
groupName: "sdk-internal",
|
||||
dependencyDashboardApproval: true
|
||||
},
|
||||
{
|
||||
matchManagers: ["dockerfile", "docker-compose"],
|
||||
@@ -63,7 +64,6 @@
|
||||
},
|
||||
{
|
||||
matchPackageNames: [
|
||||
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
|
||||
"DuoUniversal",
|
||||
"Fido2.AspNet",
|
||||
"Duende.IdentityServer",
|
||||
@@ -137,6 +137,7 @@
|
||||
"AspNetCoreRateLimit",
|
||||
"AspNetCoreRateLimit.Redis",
|
||||
"Azure.Data.Tables",
|
||||
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
|
||||
"Azure.Messaging.EventGrid",
|
||||
"Azure.Messaging.ServiceBus",
|
||||
"Azure.Storage.Blobs",
|
||||
|
||||
49
.github/workflows/build.yml
vendored
49
.github/workflows/build.yml
vendored
@@ -185,13 +185,6 @@ jobs:
|
||||
- name: Log in to ACR - production subscription
|
||||
run: az acr login -n bitwardenprod
|
||||
|
||||
- name: Retrieve GitHub PAT secrets
|
||||
id: retrieve-secret-pat
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
########## Generate image tag and build Docker image ##########
|
||||
- name: Generate Docker image tag
|
||||
id: tag
|
||||
@@ -250,8 +243,6 @@ jobs:
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.image-tags.outputs.tags }}
|
||||
secrets: |
|
||||
"GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}"
|
||||
|
||||
- name: Install Cosign
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
@@ -479,20 +470,29 @@ jobs:
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Retrieve GitHub PAT secrets
|
||||
id: retrieve-secret-pat
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
keyvault: gh-org-bitwarden
|
||||
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Trigger Bitwarden Lite build
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: self-host
|
||||
|
||||
- name: Trigger Bitwarden lite build
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'bitwarden',
|
||||
@@ -520,20 +520,29 @@ jobs:
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Retrieve GitHub PAT secrets
|
||||
id: retrieve-secret-pat
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
keyvault: gh-org-bitwarden
|
||||
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: devops
|
||||
|
||||
- name: Trigger k8s deploy
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'bitwarden',
|
||||
|
||||
14
.github/workflows/repository-management.yml
vendored
14
.github/workflows/repository-management.yml
vendored
@@ -22,9 +22,7 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
@@ -32,6 +30,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
branch: ${{ steps.set-branch.outputs.branch }}
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Set branch
|
||||
id: set-branch
|
||||
@@ -89,6 +88,7 @@ jobs:
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
permission-contents: write
|
||||
|
||||
- name: Check out branch
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -212,6 +212,7 @@ jobs:
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
permission-contents: write
|
||||
|
||||
- name: Check out target ref
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -240,10 +241,5 @@ jobs:
|
||||
move_edd_db_scripts:
|
||||
name: Move EDD database scripts
|
||||
needs: cut_branch
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
permissions: {}
|
||||
uses: ./.github/workflows/_move_edd_db_scripts.yml
|
||||
secrets: inherit
|
||||
|
||||
27
.github/workflows/test-database.yml
vendored
27
.github/workflows/test-database.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
||||
docker compose --profile mssql --profile postgres --profile mysql up -d
|
||||
shell: pwsh
|
||||
|
||||
- name: Add MariaDB for Bitwarden Lite
|
||||
- name: Add MariaDB for Bitwarden lite
|
||||
# Use a different port than MySQL
|
||||
run: |
|
||||
docker run --detach --name mariadb --env MARIADB_ROOT_PASSWORD=mariadb-password -p 4306:3306 mariadb:10
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
# Default Sqlite
|
||||
BW_TEST_DATABASES__3__TYPE: "Sqlite"
|
||||
BW_TEST_DATABASES__3__CONNECTIONSTRING: "Data Source=${{ runner.temp }}/test.db"
|
||||
# Bitwarden Lite MariaDB
|
||||
# Bitwarden lite MariaDB
|
||||
BW_TEST_DATABASES__4__TYPE: "MySql"
|
||||
BW_TEST_DATABASES__4__CONNECTIONSTRING: "server=localhost;port=4306;uid=root;pwd=mariadb-password;database=vault_dev;Allow User Variables=true"
|
||||
run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
|
||||
@@ -262,3 +262,26 @@ jobs:
|
||||
working-directory: "dev"
|
||||
run: docker compose down
|
||||
shell: pwsh
|
||||
|
||||
validate-migration-naming:
|
||||
name: Validate new migration naming and order
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate new migrations for pull request
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
git fetch origin main:main
|
||||
pwsh dev/verify_migrations.ps1 -BaseRef main
|
||||
shell: pwsh
|
||||
|
||||
- name: Validate new migrations for push
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
run: pwsh dev/verify_migrations.ps1 -BaseRef HEAD~1
|
||||
shell: pwsh
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -234,6 +234,7 @@ bitwarden_license/src/Sso/Sso.zip
|
||||
/identity.json
|
||||
/api.json
|
||||
/api.public.json
|
||||
.serena/
|
||||
|
||||
# Serena
|
||||
.serena/
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2025.11.1</Version>
|
||||
<Version>2025.12.2</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
<MicrosoftNetTestSdkVersion>17.8.0</MicrosoftNetTestSdkVersion>
|
||||
<MicrosoftNetTestSdkVersion>18.0.1</MicrosoftNetTestSdkVersion>
|
||||
|
||||
<XUnitVersion>2.6.6</XUnitVersion>
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
await _providerBillingService.CreateCustomerForClientOrganization(provider, organization);
|
||||
}
|
||||
|
||||
var customer = await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
||||
var customer = await _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
||||
{
|
||||
Description = string.Empty,
|
||||
Email = organization.BillingEmail,
|
||||
@@ -138,7 +138,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
|
||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
|
||||
var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||
var subscription = await _stripeAdapter.CreateSubscriptionAsync(subscriptionCreateOptions);
|
||||
|
||||
organization.GatewaySubscriptionId = subscription.Id;
|
||||
organization.Status = OrganizationStatusType.Created;
|
||||
@@ -148,27 +148,26 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
}
|
||||
else if (organization.IsStripeEnabled())
|
||||
{
|
||||
var subscription = await _stripeAdapter.SubscriptionGetAsync(organization.GatewaySubscriptionId, new SubscriptionGetOptions
|
||||
var subscription = await _stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId, new SubscriptionGetOptions
|
||||
{
|
||||
Expand = ["customer"]
|
||||
});
|
||||
|
||||
if (subscription.Status is StripeConstants.SubscriptionStatus.Canceled or StripeConstants.SubscriptionStatus.IncompleteExpired)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _stripeAdapter.CustomerUpdateAsync(subscription.CustomerId, new CustomerUpdateOptions
|
||||
await _stripeAdapter.UpdateCustomerAsync(subscription.CustomerId, new CustomerUpdateOptions
|
||||
{
|
||||
Email = organization.BillingEmail
|
||||
});
|
||||
|
||||
if (subscription.Customer.Discount?.Coupon != null)
|
||||
{
|
||||
await _stripeAdapter.CustomerDeleteDiscountAsync(subscription.CustomerId);
|
||||
await _stripeAdapter.DeleteCustomerDiscountAsync(subscription.CustomerId);
|
||||
}
|
||||
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, new SubscriptionUpdateOptions
|
||||
await _stripeAdapter.UpdateSubscriptionAsync(organization.GatewaySubscriptionId, new SubscriptionUpdateOptions
|
||||
{
|
||||
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
|
||||
DaysUntilDue = 30,
|
||||
|
||||
@@ -9,12 +9,16 @@ using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -59,6 +63,7 @@ public class ProviderService : IProviderService
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IProviderClientOrganizationSignUpCommand _providerClientOrganizationSignUpCommand;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
|
||||
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
|
||||
@@ -68,7 +73,8 @@ public class ProviderService : IProviderService
|
||||
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,
|
||||
IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,
|
||||
IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient,
|
||||
IProviderClientOrganizationSignUpCommand providerClientOrganizationSignUpCommand)
|
||||
IProviderClientOrganizationSignUpCommand providerClientOrganizationSignUpCommand,
|
||||
IPolicyRequirementQuery policyRequirementQuery)
|
||||
{
|
||||
_providerRepository = providerRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
@@ -89,6 +95,7 @@ public class ProviderService : IProviderService
|
||||
_providerBillingService = providerBillingService;
|
||||
_pricingClient = pricingClient;
|
||||
_providerClientOrganizationSignUpCommand = providerClientOrganizationSignUpCommand;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
}
|
||||
|
||||
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress)
|
||||
@@ -116,6 +123,18 @@ public class ProviderService : IProviderService
|
||||
throw new BadRequestException("Invalid owner.");
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
|
||||
{
|
||||
var organizationAutoConfirmPolicyRequirement = await _policyRequirementQuery
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(ownerUserId);
|
||||
|
||||
if (organizationAutoConfirmPolicyRequirement
|
||||
.CannotCreateProvider())
|
||||
{
|
||||
throw new BadRequestException(new UserCannotJoinProvider().Message);
|
||||
}
|
||||
}
|
||||
|
||||
var customer = await _providerBillingService.SetupCustomer(provider, paymentMethod, billingAddress);
|
||||
provider.GatewayCustomerId = customer.Id;
|
||||
var subscription = await _providerBillingService.SetupSubscription(provider);
|
||||
@@ -248,6 +267,18 @@ public class ProviderService : IProviderService
|
||||
throw new BadRequestException("User email does not match invite.");
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
|
||||
{
|
||||
var organizationAutoConfirmPolicyRequirement = await _policyRequirementQuery
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id);
|
||||
|
||||
if (organizationAutoConfirmPolicyRequirement
|
||||
.CannotJoinProvider())
|
||||
{
|
||||
throw new BadRequestException(new UserCannotJoinProvider().Message);
|
||||
}
|
||||
}
|
||||
|
||||
providerUser.Status = ProviderUserStatusType.Accepted;
|
||||
providerUser.UserId = user.Id;
|
||||
providerUser.Email = null;
|
||||
@@ -293,6 +324,19 @@ public class ProviderService : IProviderService
|
||||
throw new BadRequestException("Invalid user.");
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
|
||||
{
|
||||
var organizationAutoConfirmPolicyRequirement = await _policyRequirementQuery
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id);
|
||||
|
||||
if (organizationAutoConfirmPolicyRequirement
|
||||
.CannotJoinProvider())
|
||||
{
|
||||
result.Add(Tuple.Create(providerUser, new UserCannotJoinProvider().Message));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
providerUser.Status = ProviderUserStatusType.Confirmed;
|
||||
providerUser.Key = keys[providerUser.Id];
|
||||
providerUser.Email = null;
|
||||
@@ -427,7 +471,7 @@ public class ProviderService : IProviderService
|
||||
|
||||
if (!string.IsNullOrEmpty(organization.GatewayCustomerId))
|
||||
{
|
||||
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
||||
await _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
||||
{
|
||||
Email = provider.BillingEmail
|
||||
});
|
||||
@@ -487,7 +531,7 @@ public class ProviderService : IProviderService
|
||||
|
||||
private async Task<SubscriptionItem> GetSubscriptionItemAsync(string subscriptionId, string oldPlanId)
|
||||
{
|
||||
var subscriptionDetails = await _stripeAdapter.SubscriptionGetAsync(subscriptionId);
|
||||
var subscriptionDetails = await _stripeAdapter.GetSubscriptionAsync(subscriptionId);
|
||||
return subscriptionDetails.Items.Data.FirstOrDefault(item => item.Price.Id == oldPlanId);
|
||||
}
|
||||
|
||||
@@ -497,7 +541,7 @@ public class ProviderService : IProviderService
|
||||
{
|
||||
if (subscriptionItem.Price.Id != extractedPlanType)
|
||||
{
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(subscriptionItem.Subscription,
|
||||
await _stripeAdapter.UpdateSubscriptionAsync(subscriptionItem.Subscription,
|
||||
new Stripe.SubscriptionUpdateOptions
|
||||
{
|
||||
Items = new List<Stripe.SubscriptionItemOptions>
|
||||
|
||||
@@ -4,7 +4,6 @@ using Bit.Core.Billing.Providers.Models;
|
||||
using Bit.Core.Billing.Providers.Queries;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Services;
|
||||
using Stripe;
|
||||
using Stripe.Tax;
|
||||
|
||||
@@ -76,8 +75,8 @@ public class GetProviderWarningsQuery(
|
||||
|
||||
// Get active and scheduled registrations
|
||||
var registrations = (await Task.WhenAll(
|
||||
stripeAdapter.TaxRegistrationsListAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Active }),
|
||||
stripeAdapter.TaxRegistrationsListAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Scheduled })))
|
||||
stripeAdapter.ListTaxRegistrationsAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Active }),
|
||||
stripeAdapter.ListTaxRegistrationsAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Scheduled })))
|
||||
.SelectMany(registrations => registrations.Data);
|
||||
|
||||
// Find the matching registration for the customer
|
||||
|
||||
@@ -101,7 +101,7 @@ public class BusinessUnitConverter(
|
||||
providerUser.Status = ProviderUserStatusType.Confirmed;
|
||||
|
||||
// Stripe requires that we clear all the custom fields from the invoice settings if we want to replace them.
|
||||
await stripeAdapter.CustomerUpdateAsync(subscription.CustomerId, new CustomerUpdateOptions
|
||||
await stripeAdapter.UpdateCustomerAsync(subscription.CustomerId, new CustomerUpdateOptions
|
||||
{
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
@@ -116,7 +116,7 @@ public class BusinessUnitConverter(
|
||||
["convertedFrom"] = organization.Id.ToString()
|
||||
};
|
||||
|
||||
var updateCustomer = stripeAdapter.CustomerUpdateAsync(subscription.CustomerId, new CustomerUpdateOptions
|
||||
var updateCustomer = stripeAdapter.UpdateCustomerAsync(subscription.CustomerId, new CustomerUpdateOptions
|
||||
{
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
@@ -148,7 +148,7 @@ public class BusinessUnitConverter(
|
||||
|
||||
// Replace the existing password manager price with the new business unit price.
|
||||
var updateSubscription =
|
||||
stripeAdapter.SubscriptionUpdateAsync(subscription.Id,
|
||||
stripeAdapter.UpdateSubscriptionAsync(subscription.Id,
|
||||
new SubscriptionUpdateOptions
|
||||
{
|
||||
Items = [
|
||||
|
||||
@@ -61,11 +61,11 @@ public class ProviderBillingService(
|
||||
Organization organization,
|
||||
string key)
|
||||
{
|
||||
await stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
|
||||
await stripeAdapter.UpdateSubscriptionAsync(organization.GatewaySubscriptionId,
|
||||
new SubscriptionUpdateOptions { CancelAtPeriodEnd = false });
|
||||
|
||||
var subscription =
|
||||
await stripeAdapter.SubscriptionCancelAsync(organization.GatewaySubscriptionId,
|
||||
await stripeAdapter.CancelSubscriptionAsync(organization.GatewaySubscriptionId,
|
||||
new SubscriptionCancelOptions
|
||||
{
|
||||
CancellationDetails = new SubscriptionCancellationDetailsOptions
|
||||
@@ -83,7 +83,7 @@ public class ProviderBillingService(
|
||||
|
||||
if (!wasTrialing && subscription.LatestInvoice.Status == InvoiceStatus.Draft)
|
||||
{
|
||||
await stripeAdapter.InvoiceFinalizeInvoiceAsync(subscription.LatestInvoiceId,
|
||||
await stripeAdapter.FinalizeInvoiceAsync(subscription.LatestInvoiceId,
|
||||
new InvoiceFinalizeOptions { AutoAdvance = true });
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ public class ProviderBillingService(
|
||||
|
||||
if (clientCustomer.Balance != 0)
|
||||
{
|
||||
await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId,
|
||||
await stripeAdapter.CreateCustomerBalanceTransactionAsync(provider.GatewayCustomerId,
|
||||
new CustomerBalanceTransactionCreateOptions
|
||||
{
|
||||
Amount = clientCustomer.Balance,
|
||||
@@ -187,7 +187,7 @@ public class ProviderBillingService(
|
||||
]
|
||||
};
|
||||
|
||||
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId, updateOptions);
|
||||
await stripeAdapter.UpdateSubscriptionAsync(provider.GatewaySubscriptionId, updateOptions);
|
||||
|
||||
// Refactor later to ?ChangeClientPlanCommand? (ProviderPlanId, ProviderId, OrganizationId)
|
||||
// 1. Retrieve PlanType and PlanName for ProviderPlan
|
||||
@@ -275,7 +275,7 @@ public class ProviderBillingService(
|
||||
customerCreateOptions.TaxExempt = TaxExempt.Reverse;
|
||||
}
|
||||
|
||||
var customer = await stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||
var customer = await stripeAdapter.CreateCustomerAsync(customerCreateOptions);
|
||||
|
||||
organization.GatewayCustomerId = customer.Id;
|
||||
|
||||
@@ -525,7 +525,7 @@ public class ProviderBillingService(
|
||||
case TokenizablePaymentMethodType.BankAccount:
|
||||
{
|
||||
var setupIntent =
|
||||
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions
|
||||
(await stripeAdapter.ListSetupIntentsAsync(new SetupIntentListOptions
|
||||
{
|
||||
PaymentMethod = paymentMethod.Token
|
||||
}))
|
||||
@@ -558,7 +558,7 @@ public class ProviderBillingService(
|
||||
|
||||
try
|
||||
{
|
||||
return await stripeAdapter.CustomerCreateAsync(options);
|
||||
return await stripeAdapter.CreateCustomerAsync(options);
|
||||
}
|
||||
catch (StripeException stripeException) when (stripeException.StripeError?.Code == ErrorCodes.TaxIdInvalid)
|
||||
{
|
||||
@@ -580,7 +580,7 @@ public class ProviderBillingService(
|
||||
case TokenizablePaymentMethodType.BankAccount:
|
||||
{
|
||||
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(provider.Id);
|
||||
await stripeAdapter.SetupIntentCancel(setupIntentId,
|
||||
await stripeAdapter.CancelSetupIntentAsync(setupIntentId,
|
||||
new SetupIntentCancelOptions { CancellationReason = "abandoned" });
|
||||
await setupIntentCache.RemoveSetupIntentForSubscriber(provider.Id);
|
||||
break;
|
||||
@@ -638,7 +638,7 @@ public class ProviderBillingService(
|
||||
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(provider.Id);
|
||||
|
||||
var setupIntent = !string.IsNullOrEmpty(setupIntentId)
|
||||
? await stripeAdapter.SetupIntentGet(setupIntentId,
|
||||
? await stripeAdapter.GetSetupIntentAsync(setupIntentId,
|
||||
new SetupIntentGetOptions { Expand = ["payment_method"] })
|
||||
: null;
|
||||
|
||||
@@ -673,7 +673,7 @@ public class ProviderBillingService(
|
||||
|
||||
try
|
||||
{
|
||||
var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||
var subscription = await stripeAdapter.CreateSubscriptionAsync(subscriptionCreateOptions);
|
||||
|
||||
if (subscription is
|
||||
{
|
||||
@@ -708,7 +708,7 @@ public class ProviderBillingService(
|
||||
subscriberService.UpdatePaymentSource(provider, tokenizedPaymentSource),
|
||||
subscriberService.UpdateTaxInformation(provider, taxInformation));
|
||||
|
||||
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
||||
await stripeAdapter.UpdateSubscriptionAsync(provider.GatewaySubscriptionId,
|
||||
new SubscriptionUpdateOptions { CollectionMethod = CollectionMethod.ChargeAutomatically });
|
||||
}
|
||||
|
||||
@@ -791,11 +791,49 @@ public class ProviderBillingService(
|
||||
|
||||
if (subscriptionItemOptionsList.Count > 0)
|
||||
{
|
||||
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
||||
await stripeAdapter.UpdateSubscriptionAsync(provider.GatewaySubscriptionId,
|
||||
new SubscriptionUpdateOptions { Items = subscriptionItemOptionsList });
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateProviderNameAndEmail(Provider provider)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(provider.GatewayCustomerId))
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Provider ({ProviderId}) has no Stripe customer to update",
|
||||
provider.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
var newDisplayName = provider.DisplayName();
|
||||
|
||||
// Provider.DisplayName() can return null - handle gracefully
|
||||
if (string.IsNullOrWhiteSpace(newDisplayName))
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Provider ({ProviderId}) has no name to update in Stripe",
|
||||
provider.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
await stripeAdapter.UpdateCustomerAsync(provider.GatewayCustomerId,
|
||||
new CustomerUpdateOptions
|
||||
{
|
||||
Email = provider.BillingEmail,
|
||||
Description = newDisplayName,
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
CustomFields = [
|
||||
new CustomerInvoiceSettingsCustomFieldOptions
|
||||
{
|
||||
Name = provider.SubscriberType(),
|
||||
Value = newDisplayName
|
||||
}]
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private Func<int, Task> CurrySeatScalingUpdate(
|
||||
Provider provider,
|
||||
ProviderPlan providerPlan,
|
||||
@@ -807,7 +845,7 @@ public class ProviderBillingService(
|
||||
|
||||
var item = subscription.Items.First(item => item.Price.Id == priceId);
|
||||
|
||||
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId, new SubscriptionUpdateOptions
|
||||
await stripeAdapter.UpdateSubscriptionAsync(provider.GatewaySubscriptionId, new SubscriptionUpdateOptions
|
||||
{
|
||||
Items =
|
||||
[
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
using AutoMapper;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories;
|
||||
|
||||
public class SecretVersionRepository : Repository<Core.SecretsManager.Entities.SecretVersion, SecretVersion, Guid>, ISecretVersionRepository
|
||||
{
|
||||
public SecretVersionRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
|
||||
: base(serviceScopeFactory, mapper, db => db.SecretVersion)
|
||||
{ }
|
||||
|
||||
public override async Task<Core.SecretsManager.Entities.SecretVersion?> GetByIdAsync(Guid id)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var secretVersion = await dbContext.SecretVersion
|
||||
.Where(sv => sv.Id == id)
|
||||
.FirstOrDefaultAsync();
|
||||
return Mapper.Map<Core.SecretsManager.Entities.SecretVersion>(secretVersion);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.SecretVersion>> GetManyBySecretIdAsync(Guid secretId)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var secretVersions = await dbContext.SecretVersion
|
||||
.Where(sv => sv.SecretId == secretId)
|
||||
.OrderByDescending(sv => sv.VersionDate)
|
||||
.ToListAsync();
|
||||
return Mapper.Map<List<Core.SecretsManager.Entities.SecretVersion>>(secretVersions);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.SecretVersion>> GetManyByIdsAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var versionIds = ids.ToList();
|
||||
var secretVersions = await dbContext.SecretVersion
|
||||
.Where(sv => versionIds.Contains(sv.Id))
|
||||
.OrderByDescending(sv => sv.VersionDate)
|
||||
.ToListAsync();
|
||||
return Mapper.Map<List<Core.SecretsManager.Entities.SecretVersion>>(secretVersions);
|
||||
}
|
||||
|
||||
public override async Task<Core.SecretsManager.Entities.SecretVersion> CreateAsync(Core.SecretsManager.Entities.SecretVersion secretVersion)
|
||||
{
|
||||
const int maxVersionsToKeep = 10;
|
||||
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||
|
||||
// Get the IDs of the most recent (maxVersionsToKeep - 1) versions to keep
|
||||
var versionsToKeepIds = await dbContext.SecretVersion
|
||||
.Where(sv => sv.SecretId == secretVersion.SecretId)
|
||||
.OrderByDescending(sv => sv.VersionDate)
|
||||
.Take(maxVersionsToKeep - 1)
|
||||
.Select(sv => sv.Id)
|
||||
.ToListAsync();
|
||||
|
||||
// Delete all versions for this secret that are not in the "keep" list
|
||||
if (versionsToKeepIds.Any())
|
||||
{
|
||||
await dbContext.SecretVersion
|
||||
.Where(sv => sv.SecretId == secretVersion.SecretId && !versionsToKeepIds.Contains(sv.Id))
|
||||
.ExecuteDeleteAsync();
|
||||
}
|
||||
|
||||
secretVersion.SetNewId();
|
||||
var entity = Mapper.Map<SecretVersion>(secretVersion);
|
||||
|
||||
await dbContext.AddAsync(entity);
|
||||
await dbContext.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
|
||||
return secretVersion;
|
||||
}
|
||||
|
||||
public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var secretVersionIds = ids.ToList();
|
||||
await dbContext.SecretVersion
|
||||
.Where(sv => secretVersionIds.Contains(sv.Id))
|
||||
.ExecuteDeleteAsync();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ public static class SecretsManagerEfServiceCollectionExtensions
|
||||
{
|
||||
services.AddSingleton<IAccessPolicyRepository, AccessPolicyRepository>();
|
||||
services.AddSingleton<ISecretRepository, SecretRepository>();
|
||||
services.AddSingleton<ISecretVersionRepository, SecretVersionRepository>();
|
||||
services.AddSingleton<IProjectRepository, ProjectRepository>();
|
||||
services.AddSingleton<IServiceAccountRepository, ServiceAccountRepository>();
|
||||
}
|
||||
|
||||
@@ -61,17 +61,15 @@ public class GroupsController : Controller
|
||||
[HttpGet("")]
|
||||
public async Task<IActionResult> Get(
|
||||
Guid organizationId,
|
||||
[FromQuery] string filter,
|
||||
[FromQuery] int? count,
|
||||
[FromQuery] int? startIndex)
|
||||
[FromQuery] GetGroupsQueryParamModel model)
|
||||
{
|
||||
var groupsListQueryResult = await _getGroupsListQuery.GetGroupsListAsync(organizationId, filter, count, startIndex);
|
||||
var groupsListQueryResult = await _getGroupsListQuery.GetGroupsListAsync(organizationId, model);
|
||||
var scimListResponseModel = new ScimListResponseModel<ScimGroupResponseModel>
|
||||
{
|
||||
Resources = groupsListQueryResult.groupList.Select(g => new ScimGroupResponseModel(g)).ToList(),
|
||||
ItemsPerPage = count.GetValueOrDefault(groupsListQueryResult.groupList.Count()),
|
||||
ItemsPerPage = model.Count,
|
||||
TotalResults = groupsListQueryResult.totalResults,
|
||||
StartIndex = startIndex.GetValueOrDefault(1),
|
||||
StartIndex = model.StartIndex,
|
||||
};
|
||||
return Ok(scimListResponseModel);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Scim.Groups.Interfaces;
|
||||
using Bit.Scim.Models;
|
||||
|
||||
namespace Bit.Scim.Groups;
|
||||
|
||||
@@ -16,10 +17,16 @@ public class GetGroupsListQuery : IGetGroupsListQuery
|
||||
_groupRepository = groupRepository;
|
||||
}
|
||||
|
||||
public async Task<(IEnumerable<Group> groupList, int totalResults)> GetGroupsListAsync(Guid organizationId, string filter, int? count, int? startIndex)
|
||||
public async Task<(IEnumerable<Group> groupList, int totalResults)> GetGroupsListAsync(
|
||||
Guid organizationId, GetGroupsQueryParamModel groupQueryParams)
|
||||
{
|
||||
string nameFilter = null;
|
||||
string externalIdFilter = null;
|
||||
|
||||
int count = groupQueryParams.Count;
|
||||
int startIndex = groupQueryParams.StartIndex;
|
||||
string filter = groupQueryParams.Filter;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
if (filter.StartsWith("displayName eq "))
|
||||
@@ -53,11 +60,11 @@ public class GetGroupsListQuery : IGetGroupsListQuery
|
||||
}
|
||||
totalResults = groupList.Count;
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(filter) && startIndex.HasValue && count.HasValue)
|
||||
else if (string.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
groupList = groups.OrderBy(g => g.Name)
|
||||
.Skip(startIndex.Value - 1)
|
||||
.Take(count.Value)
|
||||
.Skip(startIndex - 1)
|
||||
.Take(count)
|
||||
.ToList();
|
||||
totalResults = groups.Count;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Scim.Models;
|
||||
|
||||
namespace Bit.Scim.Groups.Interfaces;
|
||||
|
||||
public interface IGetGroupsListQuery
|
||||
{
|
||||
Task<(IEnumerable<Group> groupList, int totalResults)> GetGroupsListAsync(Guid organizationId, string filter, int? count, int? startIndex);
|
||||
Task<(IEnumerable<Group> groupList, int totalResults)> GetGroupsListAsync(Guid organizationId, GetGroupsQueryParamModel model);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Scim.Models;
|
||||
|
||||
public class GetGroupsQueryParamModel
|
||||
{
|
||||
public string Filter { get; init; } = string.Empty;
|
||||
|
||||
[Range(1, int.MaxValue)]
|
||||
public int Count { get; init; } = 50;
|
||||
|
||||
[Range(1, int.MaxValue)]
|
||||
public int StartIndex { get; init; } = 1;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Scim.Models;
|
||||
|
||||
public class GetUsersQueryParamModel
|
||||
{
|
||||
public string Filter { get; init; } = string.Empty;
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Users.Interfaces;
|
||||
|
||||
namespace Bit.Scim.Users;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Scim.Models;
|
||||
|
||||
namespace Bit.Scim.Users.Interfaces;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
@@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.E
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
@@ -24,7 +25,7 @@ public class PostUserCommand(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationService organizationService,
|
||||
IPaymentService paymentService,
|
||||
IStripePaymentService paymentService,
|
||||
IScimContext scimContext,
|
||||
IFeatureService featureService,
|
||||
IInviteOrganizationUsersCommand inviteOrganizationUsersCommand,
|
||||
|
||||
@@ -201,12 +201,15 @@ public class AccountController : Controller
|
||||
returnUrl,
|
||||
state = context.Parameters["state"],
|
||||
userIdentifier = context.Parameters["session_state"],
|
||||
ssoToken
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult ExternalChallenge(string scheme, string returnUrl, string state, string userIdentifier)
|
||||
public IActionResult ExternalChallenge(string scheme, string returnUrl, string state, string userIdentifier, string ssoToken)
|
||||
{
|
||||
ValidateSchemeAgainstSsoToken(scheme, ssoToken);
|
||||
|
||||
if (string.IsNullOrEmpty(returnUrl))
|
||||
{
|
||||
returnUrl = "~/";
|
||||
@@ -235,6 +238,31 @@ public class AccountController : Controller
|
||||
return Challenge(props, scheme);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the scheme (organization ID) against the organization ID found in the ssoToken.
|
||||
/// </summary>
|
||||
/// <param name="scheme">The authentication scheme (organization ID) to validate.</param>
|
||||
/// <param name="ssoToken">The SSO token to validate against.</param>
|
||||
/// <exception cref="Exception">Thrown if the scheme (organization ID) does not match the organization ID found in the ssoToken.</exception>
|
||||
private void ValidateSchemeAgainstSsoToken(string scheme, string ssoToken)
|
||||
{
|
||||
SsoTokenable tokenable;
|
||||
|
||||
try
|
||||
{
|
||||
tokenable = _dataProtector.Unprotect(ssoToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new Exception(_i18nService.T("InvalidSsoToken"));
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(scheme, out var schemeOrgId) || tokenable.OrganizationId != schemeOrgId)
|
||||
{
|
||||
throw new Exception(_i18nService.T("SsoOrganizationIdMismatch"));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ExternalCallback()
|
||||
{
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
"connectionString": "UseDevelopmentStorage=true"
|
||||
},
|
||||
"developmentDirectory": "../../../dev",
|
||||
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
|
||||
"pricingUri": "https://billingpricing.qa.bitwarden.pw",
|
||||
"mail": {
|
||||
"smtp": {
|
||||
"host": "localhost",
|
||||
"port": 10250
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
"mail": {
|
||||
"sendGridApiKey": "SECRET",
|
||||
"amazonConfigSetName": "Email",
|
||||
"replyToEmail": "no-reply@bitwarden.com"
|
||||
"replyToEmail": "no-reply@bitwarden.com",
|
||||
"smtp": {
|
||||
"host": "localhost",
|
||||
"port": 10250
|
||||
}
|
||||
},
|
||||
"identityServer": {
|
||||
"certificateThumbprint": "SECRET"
|
||||
|
||||
@@ -131,7 +131,7 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
Arg.Is<IEnumerable<string>>(emails => emails.FirstOrDefault() == "a@example.com"));
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
|
||||
.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
|
||||
.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -156,7 +156,7 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
"b@example.com"
|
||||
]);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId, Arg.Is<SubscriptionGetOptions>(
|
||||
sutProvider.GetDependency<IStripeAdapter>().GetSubscriptionAsync(organization.GatewaySubscriptionId, Arg.Is<SubscriptionGetOptions>(
|
||||
options => options.Expand.Contains("customer")))
|
||||
.Returns(GetSubscription(organization.GatewaySubscriptionId, organization.GatewayCustomerId));
|
||||
|
||||
@@ -164,12 +164,14 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
await stripeAdapter.Received(1).CustomerUpdateAsync(organization.GatewayCustomerId,
|
||||
await stripeAdapter.Received(1).UpdateCustomerAsync(organization.GatewayCustomerId,
|
||||
Arg.Is<CustomerUpdateOptions>(options => options.Email == "a@example.com"));
|
||||
|
||||
await stripeAdapter.Received(1).CustomerDeleteDiscountAsync(organization.GatewayCustomerId);
|
||||
await stripeAdapter.Received(1).DeleteCustomerDiscountAsync(organization.GatewayCustomerId);
|
||||
|
||||
await stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
|
||||
await stripeAdapter.Received(1).DeleteCustomerDiscountAsync(organization.GatewayCustomerId);
|
||||
|
||||
await stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySubscriptionId,
|
||||
Arg.Is<SubscriptionUpdateOptions>(options =>
|
||||
options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
|
||||
options.DaysUntilDue == 30));
|
||||
@@ -226,7 +228,7 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
|
||||
stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
|
||||
options.Description == string.Empty &&
|
||||
options.Email == organization.BillingEmail &&
|
||||
options.Expand[0] == "tax" &&
|
||||
@@ -239,14 +241,14 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
}
|
||||
});
|
||||
|
||||
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
|
||||
stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
|
||||
{
|
||||
Id = "subscription_id"
|
||||
});
|
||||
|
||||
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||
|
||||
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
||||
await stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
||||
options.Customer == organization.GatewayCustomerId &&
|
||||
options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
|
||||
options.DaysUntilDue == 30 &&
|
||||
@@ -315,7 +317,7 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
|
||||
stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
|
||||
options.Description == string.Empty &&
|
||||
options.Email == organization.BillingEmail &&
|
||||
options.Expand[0] == "tax" &&
|
||||
@@ -328,14 +330,14 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
}
|
||||
});
|
||||
|
||||
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
|
||||
stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
|
||||
{
|
||||
Id = "subscription_id"
|
||||
});
|
||||
|
||||
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||
|
||||
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
||||
await stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
||||
options.Customer == organization.GatewayCustomerId &&
|
||||
options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
|
||||
options.DaysUntilDue == 30 &&
|
||||
@@ -434,7 +436,7 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Any<CustomerUpdateOptions>())
|
||||
stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerUpdateOptions>())
|
||||
.Returns(new Customer
|
||||
{
|
||||
Id = "customer_id",
|
||||
@@ -444,7 +446,7 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
}
|
||||
});
|
||||
|
||||
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
|
||||
stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
|
||||
{
|
||||
Id = "new_subscription_id"
|
||||
});
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
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;
|
||||
using Bit.Core.AdminConsole.Models.Business.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -100,6 +106,57 @@ public class ProviderServiceTests
|
||||
.ReplaceAsync(Arg.Is<ProviderUser>(pu => pu.UserId == user.Id && pu.ProviderId == provider.Id && pu.Key == key));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CompleteSetupAsync_WithAutoConfirmEnabled_ThrowsUserCannotJoinProviderError(User user, Provider provider,
|
||||
string key,
|
||||
TokenizedPaymentMethod tokenizedPaymentMethod, BillingAddress billingAddress,
|
||||
[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);
|
||||
|
||||
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
|
||||
|
||||
var customer = new Customer { Id = "customer_id" };
|
||||
providerBillingService.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress).Returns(customer);
|
||||
|
||||
var subscription = new Subscription { Id = "subscription_id" };
|
||||
providerBillingService.SetupSubscription(provider).Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
var policyDetails = new List<PolicyDetails> { new() { OrganizationId = Guid.NewGuid(), IsProvider = false } };
|
||||
var policyRequirement = new AutomaticUserConfirmationPolicyRequirement(policyDetails);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
|
||||
.Returns(policyRequirement);
|
||||
|
||||
sutProvider.Create();
|
||||
|
||||
var token = protector.Protect(
|
||||
$"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, tokenizedPaymentMethod,
|
||||
billingAddress));
|
||||
|
||||
Assert.Equal(new UserCannotJoinProvider().Message, exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_ProviderIdIsInvalid_Throws(Provider provider, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
@@ -579,6 +636,132 @@ public class ProviderServiceTests
|
||||
Assert.Equal(user.Id, pu.UserId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AcceptUserAsync_WithAutoConfirmEnabledAndPolicyExists_Throws(
|
||||
[ProviderUser(ProviderUserStatusType.Invited)] ProviderUser providerUser,
|
||||
User user,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetByIdAsync(providerUser.Id)
|
||||
.Returns(providerUser);
|
||||
|
||||
var protector = DataProtectionProvider
|
||||
.Create("ApplicationName")
|
||||
.CreateProtector("ProviderServiceDataProtector");
|
||||
|
||||
sutProvider.GetDependency<IDataProtectionProvider>()
|
||||
.CreateProtector("ProviderServiceDataProtector")
|
||||
.Returns(protector);
|
||||
|
||||
sutProvider.Create();
|
||||
|
||||
providerUser.Email = user.Email;
|
||||
var token = protector.Protect($"ProviderUserInvite {providerUser.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
var policyDetails = new List<PolicyDetails>
|
||||
{
|
||||
new() { OrganizationId = Guid.NewGuid(), IsProvider = false }
|
||||
};
|
||||
var policyRequirement = new AutomaticUserConfirmationPolicyRequirement(policyDetails);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
|
||||
.Returns(policyRequirement);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, token));
|
||||
|
||||
Assert.Equal(new UserCannotJoinProvider().Message, exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AcceptUserAsync_WithAutoConfirmEnabledButNoPolicyExists_Success(
|
||||
[ProviderUser(ProviderUserStatusType.Invited)] ProviderUser providerUser,
|
||||
User user,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetByIdAsync(providerUser.Id)
|
||||
.Returns(providerUser);
|
||||
|
||||
var protector = DataProtectionProvider
|
||||
.Create("ApplicationName")
|
||||
.CreateProtector("ProviderServiceDataProtector");
|
||||
|
||||
sutProvider.GetDependency<IDataProtectionProvider>()
|
||||
.CreateProtector("ProviderServiceDataProtector")
|
||||
.Returns(protector);
|
||||
sutProvider.Create();
|
||||
|
||||
providerUser.Email = user.Email;
|
||||
var token = protector.Protect($"ProviderUserInvite {providerUser.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequirement = new AutomaticUserConfirmationPolicyRequirement([]);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
|
||||
.Returns(policyRequirement);
|
||||
|
||||
// Act
|
||||
var pu = await sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, token);
|
||||
|
||||
// Assert
|
||||
Assert.Null(pu.Email);
|
||||
Assert.Equal(ProviderUserStatusType.Accepted, pu.Status);
|
||||
Assert.Equal(user.Id, pu.UserId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AcceptUserAsync_WithAutoConfirmDisabled_Success(
|
||||
[ProviderUser(ProviderUserStatusType.Invited)] ProviderUser providerUser,
|
||||
User user,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetByIdAsync(providerUser.Id)
|
||||
.Returns(providerUser);
|
||||
|
||||
var protector = DataProtectionProvider
|
||||
.Create("ApplicationName")
|
||||
.CreateProtector("ProviderServiceDataProtector");
|
||||
|
||||
sutProvider.GetDependency<IDataProtectionProvider>()
|
||||
.CreateProtector("ProviderServiceDataProtector")
|
||||
.Returns(protector);
|
||||
sutProvider.Create();
|
||||
|
||||
providerUser.Email = user.Email;
|
||||
var token = protector.Protect($"ProviderUserInvite {providerUser.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var pu = await sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, token);
|
||||
|
||||
// Assert
|
||||
Assert.Null(pu.Email);
|
||||
Assert.Equal(ProviderUserStatusType.Accepted, pu.Status);
|
||||
Assert.Equal(user.Id, pu.UserId);
|
||||
|
||||
// Verify that policy check was never called when feature flag is disabled
|
||||
await sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.DidNotReceive()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUsersAsync_NoValid(
|
||||
[ProviderUser(ProviderUserStatusType.Invited)] ProviderUser pu1,
|
||||
@@ -625,13 +808,131 @@ public class ProviderServiceTests
|
||||
Assert.Equal("Invalid user.", result[2].Item2);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUsersAsync_WithAutoConfirmEnabledAndPolicyExists_ReturnsError(
|
||||
[ProviderUser(ProviderUserStatusType.Accepted)] ProviderUser pu1, User u1,
|
||||
Provider provider, User confirmingUser, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
pu1.ProviderId = provider.Id;
|
||||
pu1.UserId = u1.Id;
|
||||
var providerUsers = new[] { pu1 };
|
||||
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetManyAsync([]).ReturnsForAnyArgs(providerUsers);
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([u1]);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
var policyDetails = new List<PolicyDetails>
|
||||
{
|
||||
new() { OrganizationId = Guid.NewGuid(), IsProvider = false }
|
||||
};
|
||||
var policyRequirement = new AutomaticUserConfirmationPolicyRequirement(policyDetails);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(u1.Id)
|
||||
.Returns(policyRequirement);
|
||||
|
||||
var dict = providerUsers.ToDictionary(pu => pu.Id, _ => "key");
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ConfirmUsersAsync(pu1.ProviderId, dict, confirmingUser.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(new UserCannotJoinProvider().Message, result[0].Item2);
|
||||
|
||||
// Verify user was not confirmed
|
||||
await providerUserRepository.DidNotReceive().ReplaceAsync(Arg.Any<ProviderUser>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUsersAsync_WithAutoConfirmEnabledButNoPolicyExists_Success(
|
||||
[ProviderUser(ProviderUserStatusType.Accepted)] ProviderUser pu1, User u1,
|
||||
Provider provider, User confirmingUser, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
pu1.ProviderId = provider.Id;
|
||||
pu1.UserId = u1.Id;
|
||||
var providerUsers = new[] { pu1 };
|
||||
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetManyAsync([]).ReturnsForAnyArgs(providerUsers);
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([u1]);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequirement = new AutomaticUserConfirmationPolicyRequirement(new List<PolicyDetails>());
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(u1.Id)
|
||||
.Returns(policyRequirement);
|
||||
|
||||
var dict = providerUsers.ToDictionary(pu => pu.Id, _ => "key");
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ConfirmUsersAsync(pu1.ProviderId, dict, confirmingUser.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("", result[0].Item2);
|
||||
|
||||
// Verify user was confirmed
|
||||
await providerUserRepository.Received(1).ReplaceAsync(Arg.Is<ProviderUser>(pu =>
|
||||
pu.Status == ProviderUserStatusType.Confirmed));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUsersAsync_WithAutoConfirmDisabled_Success(
|
||||
[ProviderUser(ProviderUserStatusType.Accepted)] ProviderUser pu1, User u1,
|
||||
Provider provider, User confirmingUser, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
pu1.ProviderId = provider.Id;
|
||||
pu1.UserId = u1.Id;
|
||||
var providerUsers = new[] { pu1 };
|
||||
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetManyAsync([]).ReturnsForAnyArgs(providerUsers);
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([u1]);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(false);
|
||||
|
||||
var dict = providerUsers.ToDictionary(pu => pu.Id, _ => "key");
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ConfirmUsersAsync(pu1.ProviderId, dict, confirmingUser.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("", result[0].Item2);
|
||||
|
||||
// Verify user was confirmed
|
||||
await providerUserRepository.Received(1).ReplaceAsync(Arg.Is<ProviderUser>(pu =>
|
||||
pu.Status == ProviderUserStatusType.Confirmed));
|
||||
|
||||
// Verify that policy check was never called when feature flag is disabled
|
||||
await sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.DidNotReceive()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveUserAsync_UserIdIsInvalid_Throws(ProviderUser providerUser,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
providerUser.Id = default;
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveUserAsync(providerUser, default));
|
||||
providerUser.Id = Guid.Empty;
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.SaveUserAsync(providerUser, Guid.Empty));
|
||||
Assert.Equal("Invite the user first.", exception.Message);
|
||||
}
|
||||
|
||||
@@ -757,7 +1058,7 @@ public class ProviderServiceTests
|
||||
await organizationRepository.Received(1)
|
||||
.ReplaceAsync(Arg.Is<Organization>(org => org.BillingEmail == provider.BillingEmail));
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerUpdateAsync(
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).UpdateCustomerAsync(
|
||||
organization.GatewayCustomerId,
|
||||
Arg.Is<CustomerUpdateOptions>(options => options.Email == provider.BillingEmail));
|
||||
|
||||
@@ -828,9 +1129,9 @@ public class ProviderServiceTests
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
var subscriptionItem = GetSubscription(organization.GatewaySubscriptionId);
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
|
||||
sutProvider.GetDependency<IStripeAdapter>().GetSubscriptionAsync(organization.GatewaySubscriptionId)
|
||||
.Returns(GetSubscription(organization.GatewaySubscriptionId));
|
||||
await sutProvider.GetDependency<IStripeAdapter>().SubscriptionUpdateAsync(
|
||||
await sutProvider.GetDependency<IStripeAdapter>().UpdateSubscriptionAsync(
|
||||
organization.GatewaySubscriptionId, SubscriptionUpdateRequest(expectedPlanId, subscriptionItem));
|
||||
|
||||
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
||||
|
||||
@@ -3,7 +3,6 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@@ -63,7 +62,7 @@ public class GetProviderWarningsQueryTests
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration> { Data = [] });
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
@@ -95,7 +94,7 @@ public class GetProviderWarningsQueryTests
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration> { Data = [] });
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
@@ -129,7 +128,7 @@ public class GetProviderWarningsQueryTests
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(false);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration> { Data = [] });
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
@@ -163,7 +162,7 @@ public class GetProviderWarningsQueryTests
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration> { Data = [] });
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
@@ -224,7 +223,7 @@ public class GetProviderWarningsQueryTests
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "GB" }]
|
||||
@@ -257,7 +256,7 @@ public class GetProviderWarningsQueryTests
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "CA" }]
|
||||
@@ -296,7 +295,7 @@ public class GetProviderWarningsQueryTests
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "CA" }]
|
||||
@@ -338,7 +337,7 @@ public class GetProviderWarningsQueryTests
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "CA" }]
|
||||
@@ -383,7 +382,7 @@ public class GetProviderWarningsQueryTests
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "CA" }]
|
||||
@@ -428,7 +427,7 @@ public class GetProviderWarningsQueryTests
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "CA" }]
|
||||
@@ -461,7 +460,7 @@ public class GetProviderWarningsQueryTests
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Is<RegistrationListOptions>(opt => opt.Status == TaxRegistrationStatus.Active))
|
||||
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Is<RegistrationListOptions>(opt => opt.Status == TaxRegistrationStatus.Active))
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [
|
||||
@@ -470,7 +469,7 @@ public class GetProviderWarningsQueryTests
|
||||
new Registration { Country = "FR" }
|
||||
]
|
||||
});
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Is<RegistrationListOptions>(opt => opt.Status == TaxRegistrationStatus.Scheduled))
|
||||
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Is<RegistrationListOptions>(opt => opt.Status == TaxRegistrationStatus.Scheduled))
|
||||
.Returns(new StripeList<Registration> { Data = [] });
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
@@ -505,7 +504,7 @@ public class GetProviderWarningsQueryTests
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "CA" }]
|
||||
@@ -543,7 +542,7 @@ public class GetProviderWarningsQueryTests
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "US" }]
|
||||
|
||||
@@ -144,11 +144,11 @@ public class BusinessUnitConverterTests
|
||||
|
||||
await businessUnitConverter.FinalizeConversion(organization, userId, token, providerKey, organizationKey);
|
||||
|
||||
await _stripeAdapter.Received(2).CustomerUpdateAsync(subscription.CustomerId, Arg.Any<CustomerUpdateOptions>());
|
||||
await _stripeAdapter.Received(2).UpdateCustomerAsync(subscription.CustomerId, Arg.Any<CustomerUpdateOptions>());
|
||||
|
||||
var updatedPriceId = ProviderPriceAdapter.GetActivePriceId(provider, enterpriseAnnually.Type);
|
||||
|
||||
await _stripeAdapter.Received(1).SubscriptionUpdateAsync(subscription.Id, Arg.Is<SubscriptionUpdateOptions>(
|
||||
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id, Arg.Is<SubscriptionUpdateOptions>(
|
||||
arguments =>
|
||||
arguments.Items.Count == 2 &&
|
||||
arguments.Items[0].Id == "subscription_item_id" &&
|
||||
|
||||
@@ -20,7 +20,6 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
@@ -85,7 +84,7 @@ public class ProviderBillingServiceTests
|
||||
|
||||
// Assert
|
||||
await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any<ProviderPlan>());
|
||||
await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
|
||||
await stripeAdapter.Received(0).UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -113,7 +112,7 @@ public class ProviderBillingServiceTests
|
||||
|
||||
// Assert
|
||||
await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any<ProviderPlan>());
|
||||
await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
|
||||
await stripeAdapter.Received(0).UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -180,14 +179,14 @@ public class ProviderBillingServiceTests
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
await stripeAdapter.Received(1)
|
||||
.SubscriptionUpdateAsync(
|
||||
.UpdateSubscriptionAsync(
|
||||
Arg.Is(provider.GatewaySubscriptionId),
|
||||
Arg.Is<SubscriptionUpdateOptions>(p =>
|
||||
p.Items.Count(si => si.Id == "si_ent_annual" && si.Deleted == true) == 1));
|
||||
|
||||
var newPlanCfg = MockPlans.Get(command.NewPlan);
|
||||
await stripeAdapter.Received(1)
|
||||
.SubscriptionUpdateAsync(
|
||||
.UpdateSubscriptionAsync(
|
||||
Arg.Is(provider.GatewaySubscriptionId),
|
||||
Arg.Is<SubscriptionUpdateOptions>(p =>
|
||||
p.Items.Count(si =>
|
||||
@@ -268,7 +267,7 @@ public class ProviderBillingServiceTests
|
||||
CloudRegion = "US"
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(
|
||||
sutProvider.GetDependency<IStripeAdapter>().CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(
|
||||
options =>
|
||||
options.Address.Country == providerCustomer.Address.Country &&
|
||||
options.Address.PostalCode == providerCustomer.Address.PostalCode &&
|
||||
@@ -288,7 +287,7 @@ public class ProviderBillingServiceTests
|
||||
|
||||
await sutProvider.Sut.CreateCustomerForClientOrganization(provider, organization);
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(
|
||||
options =>
|
||||
options.Address.Country == providerCustomer.Address.Country &&
|
||||
options.Address.PostalCode == providerCustomer.Address.PostalCode &&
|
||||
@@ -349,7 +348,7 @@ public class ProviderBillingServiceTests
|
||||
CloudRegion = "US"
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(
|
||||
sutProvider.GetDependency<IStripeAdapter>().CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(
|
||||
options =>
|
||||
options.Address.Country == providerCustomer.Address.Country &&
|
||||
options.Address.PostalCode == providerCustomer.Address.PostalCode &&
|
||||
@@ -370,7 +369,7 @@ public class ProviderBillingServiceTests
|
||||
|
||||
await sutProvider.Sut.CreateCustomerForClientOrganization(provider, organization);
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(
|
||||
options =>
|
||||
options.Address.Country == providerCustomer.Address.Country &&
|
||||
options.Address.PostalCode == providerCustomer.Address.PostalCode &&
|
||||
@@ -535,7 +534,7 @@ public class ProviderBillingServiceTests
|
||||
await sutProvider.Sut.ScaleSeats(provider, PlanType.TeamsMonthly, 10);
|
||||
|
||||
// 50 assigned seats + 10 seat scale up = 60 seats, well below the 100 minimum
|
||||
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs().SubscriptionUpdateAsync(
|
||||
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs().UpdateSubscriptionAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<SubscriptionUpdateOptions>());
|
||||
|
||||
@@ -619,7 +618,7 @@ public class ProviderBillingServiceTests
|
||||
await sutProvider.Sut.ScaleSeats(provider, PlanType.TeamsMonthly, 10);
|
||||
|
||||
// 95 current + 10 seat scale = 105 seats, 5 above the minimum
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).SubscriptionUpdateAsync(
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).UpdateSubscriptionAsync(
|
||||
provider.GatewaySubscriptionId,
|
||||
Arg.Is<SubscriptionUpdateOptions>(
|
||||
options =>
|
||||
@@ -707,7 +706,7 @@ public class ProviderBillingServiceTests
|
||||
await sutProvider.Sut.ScaleSeats(provider, PlanType.TeamsMonthly, 10);
|
||||
|
||||
// 110 current + 10 seat scale up = 120 seats
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).SubscriptionUpdateAsync(
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).UpdateSubscriptionAsync(
|
||||
provider.GatewaySubscriptionId,
|
||||
Arg.Is<SubscriptionUpdateOptions>(
|
||||
options =>
|
||||
@@ -795,7 +794,7 @@ public class ProviderBillingServiceTests
|
||||
await sutProvider.Sut.ScaleSeats(provider, PlanType.TeamsMonthly, -30);
|
||||
|
||||
// 110 seats - 30 scale down seats = 80 seats, below the 100 seat minimum.
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).SubscriptionUpdateAsync(
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).UpdateSubscriptionAsync(
|
||||
provider.GatewaySubscriptionId,
|
||||
Arg.Is<SubscriptionUpdateOptions>(
|
||||
options =>
|
||||
@@ -914,12 +913,12 @@ public class ProviderBillingServiceTests
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = "token" };
|
||||
|
||||
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
|
||||
stripeAdapter.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>
|
||||
options.PaymentMethod == tokenizedPaymentMethod.Token)).Returns([
|
||||
new SetupIntent { Id = "setup_intent_id" }
|
||||
]);
|
||||
|
||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
stripeAdapter.CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
o.Address.Country == billingAddress.Country &&
|
||||
o.Address.PostalCode == billingAddress.PostalCode &&
|
||||
o.Address.Line1 == billingAddress.Line1 &&
|
||||
@@ -942,7 +941,7 @@ public class ProviderBillingServiceTests
|
||||
|
||||
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_id");
|
||||
|
||||
await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_id", Arg.Is<SetupIntentCancelOptions>(options =>
|
||||
await stripeAdapter.Received(1).CancelSetupIntentAsync("setup_intent_id", Arg.Is<SetupIntentCancelOptions>(options =>
|
||||
options.CancellationReason == "abandoned"));
|
||||
|
||||
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).RemoveSetupIntentForSubscriber(provider.Id);
|
||||
@@ -964,7 +963,7 @@ public class ProviderBillingServiceTests
|
||||
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentMethod.Token)
|
||||
.Returns("braintree_customer_id");
|
||||
|
||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
stripeAdapter.CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
o.Address.Country == billingAddress.Country &&
|
||||
o.Address.PostalCode == billingAddress.PostalCode &&
|
||||
o.Address.Line1 == billingAddress.Line1 &&
|
||||
@@ -1007,12 +1006,12 @@ public class ProviderBillingServiceTests
|
||||
|
||||
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = "token" };
|
||||
|
||||
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
|
||||
stripeAdapter.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>
|
||||
options.PaymentMethod == tokenizedPaymentMethod.Token)).Returns([
|
||||
new SetupIntent { Id = "setup_intent_id" }
|
||||
]);
|
||||
|
||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
stripeAdapter.CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
o.Address.Country == billingAddress.Country &&
|
||||
o.Address.PostalCode == billingAddress.PostalCode &&
|
||||
o.Address.Line1 == billingAddress.Line1 &&
|
||||
@@ -1058,7 +1057,7 @@ public class ProviderBillingServiceTests
|
||||
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentMethod.Token)
|
||||
.Returns("braintree_customer_id");
|
||||
|
||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
stripeAdapter.CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
o.Address.Country == billingAddress.Country &&
|
||||
o.Address.PostalCode == billingAddress.PostalCode &&
|
||||
o.Address.Line1 == billingAddress.Line1 &&
|
||||
@@ -1100,7 +1099,7 @@ public class ProviderBillingServiceTests
|
||||
|
||||
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = "token" };
|
||||
|
||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
stripeAdapter.CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
o.Address.Country == billingAddress.Country &&
|
||||
o.Address.PostalCode == billingAddress.PostalCode &&
|
||||
o.Address.Line1 == billingAddress.Line1 &&
|
||||
@@ -1142,7 +1141,7 @@ public class ProviderBillingServiceTests
|
||||
|
||||
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = "token" };
|
||||
|
||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
stripeAdapter.CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
o.Address.Country == billingAddress.Country &&
|
||||
o.Address.PostalCode == billingAddress.PostalCode &&
|
||||
o.Address.Line1 == billingAddress.Line1 &&
|
||||
@@ -1178,7 +1177,7 @@ public class ProviderBillingServiceTests
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = "token" };
|
||||
|
||||
stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>())
|
||||
stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>())
|
||||
.Throws(new StripeException("Invalid tax ID") { StripeError = new StripeError { Code = "tax_id_invalid" } });
|
||||
|
||||
var actual = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
@@ -1216,7 +1215,7 @@ public class ProviderBillingServiceTests
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
|
||||
.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -1244,7 +1243,7 @@ public class ProviderBillingServiceTests
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
|
||||
.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -1272,7 +1271,7 @@ public class ProviderBillingServiceTests
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
|
||||
.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -1323,7 +1322,7 @@ public class ProviderBillingServiceTests
|
||||
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
|
||||
.Returns(providerPlans);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>())
|
||||
sutProvider.GetDependency<IStripeAdapter>().CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>())
|
||||
.Returns(
|
||||
new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Incomplete });
|
||||
|
||||
@@ -1381,7 +1380,7 @@ public class ProviderBillingServiceTests
|
||||
|
||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||
sutProvider.GetDependency<IStripeAdapter>().CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||
sub =>
|
||||
sub.AutomaticTax.Enabled == true &&
|
||||
sub.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
|
||||
@@ -1458,7 +1457,7 @@ public class ProviderBillingServiceTests
|
||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||
sutProvider.GetDependency<IStripeAdapter>().CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||
sub =>
|
||||
sub.AutomaticTax.Enabled == true &&
|
||||
sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically &&
|
||||
@@ -1538,7 +1537,7 @@ public class ProviderBillingServiceTests
|
||||
|
||||
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntentId);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntentId, Arg.Is<SetupIntentGetOptions>(options =>
|
||||
sutProvider.GetDependency<IStripeAdapter>().GetSetupIntentAsync(setupIntentId, Arg.Is<SetupIntentGetOptions>(options =>
|
||||
options.Expand.Contains("payment_method"))).Returns(new SetupIntent
|
||||
{
|
||||
Id = setupIntentId,
|
||||
@@ -1553,7 +1552,7 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||
sutProvider.GetDependency<IStripeAdapter>().CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||
sub =>
|
||||
sub.AutomaticTax.Enabled == true &&
|
||||
sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically &&
|
||||
@@ -1635,7 +1634,7 @@ public class ProviderBillingServiceTests
|
||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||
sutProvider.GetDependency<IStripeAdapter>().CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||
sub =>
|
||||
sub.AutomaticTax.Enabled == true &&
|
||||
sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically &&
|
||||
@@ -1713,7 +1712,7 @@ public class ProviderBillingServiceTests
|
||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||
sutProvider.GetDependency<IStripeAdapter>().CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||
sub =>
|
||||
sub.AutomaticTax.Enabled == true &&
|
||||
sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically &&
|
||||
@@ -1828,7 +1827,7 @@ public class ProviderBillingServiceTests
|
||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||
providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly && providerPlan.SeatMinimum == 20 && providerPlan.PurchasedSeats == 5));
|
||||
|
||||
await stripeAdapter.Received(1).SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
||||
await stripeAdapter.Received(1).UpdateSubscriptionAsync(provider.GatewaySubscriptionId,
|
||||
Arg.Is<SubscriptionUpdateOptions>(
|
||||
options =>
|
||||
options.Items.Count == 2 &&
|
||||
@@ -1908,7 +1907,7 @@ public class ProviderBillingServiceTests
|
||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||
providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly && providerPlan.SeatMinimum == 50));
|
||||
|
||||
await stripeAdapter.Received(1).SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
||||
await stripeAdapter.Received(1).UpdateSubscriptionAsync(provider.GatewaySubscriptionId,
|
||||
Arg.Is<SubscriptionUpdateOptions>(
|
||||
options =>
|
||||
options.Items.Count == 2 &&
|
||||
@@ -1989,7 +1988,7 @@ public class ProviderBillingServiceTests
|
||||
providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly && providerPlan.SeatMinimum == 60 && providerPlan.PurchasedSeats == 10));
|
||||
|
||||
await stripeAdapter.DidNotReceiveWithAnyArgs()
|
||||
.SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
|
||||
.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -2062,7 +2061,7 @@ public class ProviderBillingServiceTests
|
||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||
providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly && providerPlan.SeatMinimum == 80 && providerPlan.PurchasedSeats == 0));
|
||||
|
||||
await stripeAdapter.Received(1).SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
||||
await stripeAdapter.Received(1).UpdateSubscriptionAsync(provider.GatewaySubscriptionId,
|
||||
Arg.Is<SubscriptionUpdateOptions>(
|
||||
options =>
|
||||
options.Items.Count == 2 &&
|
||||
@@ -2142,7 +2141,7 @@ public class ProviderBillingServiceTests
|
||||
await providerPlanRepository.DidNotReceive().ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||
providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly));
|
||||
|
||||
await stripeAdapter.Received(1).SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
||||
await stripeAdapter.Received(1).UpdateSubscriptionAsync(provider.GatewaySubscriptionId,
|
||||
Arg.Is<SubscriptionUpdateOptions>(
|
||||
options =>
|
||||
options.Items.Count == 1 &&
|
||||
@@ -2151,4 +2150,151 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateProviderNameAndEmail
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateProviderNameAndEmail_NullGatewayCustomerId_LogsWarningAndReturns(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
provider.GatewayCustomerId = null;
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateProviderNameAndEmail(provider);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.DidNotReceive().UpdateCustomerAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<CustomerUpdateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateProviderNameAndEmail_EmptyGatewayCustomerId_LogsWarningAndReturns(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
provider.GatewayCustomerId = "";
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateProviderNameAndEmail(provider);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.DidNotReceive().UpdateCustomerAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<CustomerUpdateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateProviderNameAndEmail_NullProviderName_LogsWarningAndReturns(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
provider.Name = null;
|
||||
provider.GatewayCustomerId = "cus_test123";
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateProviderNameAndEmail(provider);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.DidNotReceive().UpdateCustomerAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<CustomerUpdateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateProviderNameAndEmail_EmptyProviderName_LogsWarningAndReturns(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
provider.Name = "";
|
||||
provider.GatewayCustomerId = "cus_test123";
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateProviderNameAndEmail(provider);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.DidNotReceive().UpdateCustomerAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<CustomerUpdateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateProviderNameAndEmail_ValidProvider_CallsStripeWithCorrectParameters(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
provider.Name = "Test Provider";
|
||||
provider.BillingEmail = "billing@test.com";
|
||||
provider.GatewayCustomerId = "cus_test123";
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateProviderNameAndEmail(provider);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).UpdateCustomerAsync(
|
||||
provider.GatewayCustomerId,
|
||||
Arg.Is<CustomerUpdateOptions>(options =>
|
||||
options.Email == provider.BillingEmail &&
|
||||
options.Description == provider.Name &&
|
||||
options.InvoiceSettings.CustomFields.Count == 1 &&
|
||||
options.InvoiceSettings.CustomFields[0].Name == "Provider" &&
|
||||
options.InvoiceSettings.CustomFields[0].Value == provider.Name));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateProviderNameAndEmail_LongProviderName_UsesFullName(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var longName = new string('A', 50); // 50 characters
|
||||
provider.Name = longName;
|
||||
provider.BillingEmail = "billing@test.com";
|
||||
provider.GatewayCustomerId = "cus_test123";
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateProviderNameAndEmail(provider);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).UpdateCustomerAsync(
|
||||
provider.GatewayCustomerId,
|
||||
Arg.Is<CustomerUpdateOptions>(options =>
|
||||
options.InvoiceSettings.CustomFields[0].Value == longName));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateProviderNameAndEmail_NullBillingEmail_UpdatesWithNull(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
provider.Name = "Test Provider";
|
||||
provider.BillingEmail = null;
|
||||
provider.GatewayCustomerId = "cus_test123";
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateProviderNameAndEmail(provider);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).UpdateCustomerAsync(
|
||||
provider.GatewayCustomerId,
|
||||
Arg.Is<CustomerUpdateOptions>(options =>
|
||||
options.Email == null &&
|
||||
options.Description == provider.Name));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretsManager.Repositories;
|
||||
|
||||
public class SecretVersionRepositoryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SecretVersion_EntityCreation_Success(SecretVersion secretVersion)
|
||||
{
|
||||
// Arrange & Act
|
||||
secretVersion.SetNewId();
|
||||
|
||||
// Assert
|
||||
Assert.NotEqual(Guid.Empty, secretVersion.Id);
|
||||
Assert.NotEqual(Guid.Empty, secretVersion.SecretId);
|
||||
Assert.NotNull(secretVersion.Value);
|
||||
Assert.NotEqual(default, secretVersion.VersionDate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SecretVersion_WithServiceAccountEditor_Success(SecretVersion secretVersion, Guid serviceAccountId)
|
||||
{
|
||||
// Arrange & Act
|
||||
secretVersion.EditorServiceAccountId = serviceAccountId;
|
||||
secretVersion.EditorOrganizationUserId = null;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(serviceAccountId, secretVersion.EditorServiceAccountId);
|
||||
Assert.Null(secretVersion.EditorOrganizationUserId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SecretVersion_WithOrganizationUserEditor_Success(SecretVersion secretVersion, Guid organizationUserId)
|
||||
{
|
||||
// Arrange & Act
|
||||
secretVersion.EditorOrganizationUserId = organizationUserId;
|
||||
secretVersion.EditorServiceAccountId = null;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(organizationUserId, secretVersion.EditorOrganizationUserId);
|
||||
Assert.Null(secretVersion.EditorServiceAccountId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SecretVersion_NullableEditors_Success(SecretVersion secretVersion)
|
||||
{
|
||||
// Arrange & Act
|
||||
secretVersion.EditorServiceAccountId = null;
|
||||
secretVersion.EditorOrganizationUserId = null;
|
||||
|
||||
// Assert
|
||||
Assert.Null(secretVersion.EditorServiceAccountId);
|
||||
Assert.Null(secretVersion.EditorOrganizationUserId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SecretVersion_VersionDateSet_Success(SecretVersion secretVersion)
|
||||
{
|
||||
// Arrange
|
||||
var versionDate = DateTime.UtcNow;
|
||||
|
||||
// Act
|
||||
secretVersion.VersionDate = versionDate;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(versionDate, secretVersion.VersionDate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SecretVersion_ValueEncrypted_Success(SecretVersion secretVersion, string encryptedValue)
|
||||
{
|
||||
// Arrange & Act
|
||||
secretVersion.Value = encryptedValue;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(encryptedValue, secretVersion.Value);
|
||||
Assert.NotEmpty(secretVersion.Value);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SecretVersion_MultipleVersions_DifferentIds(List<SecretVersion> secretVersions, Guid secretId)
|
||||
{
|
||||
// Arrange & Act
|
||||
foreach (var version in secretVersions)
|
||||
{
|
||||
version.SecretId = secretId;
|
||||
version.SetNewId();
|
||||
}
|
||||
|
||||
// Assert
|
||||
var distinctIds = secretVersions.Select(v => v.Id).Distinct();
|
||||
Assert.Equal(secretVersions.Count, distinctIds.Count());
|
||||
Assert.All(secretVersions, v => Assert.Equal(secretId, v.SecretId));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SecretVersion_VersionDateOrdering_Success(SecretVersion version1, SecretVersion version2, SecretVersion version3, Guid secretId)
|
||||
{
|
||||
// Arrange
|
||||
var now = DateTime.UtcNow;
|
||||
version1.SecretId = secretId;
|
||||
version1.VersionDate = now.AddDays(-2);
|
||||
|
||||
version2.SecretId = secretId;
|
||||
version2.VersionDate = now.AddDays(-1);
|
||||
|
||||
version3.SecretId = secretId;
|
||||
version3.VersionDate = now;
|
||||
|
||||
var versions = new List<SecretVersion> { version2, version3, version1 };
|
||||
|
||||
// Act
|
||||
var orderedVersions = versions.OrderByDescending(v => v.VersionDate).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(version3.Id, orderedVersions[0].Id); // Most recent
|
||||
Assert.Equal(version2.Id, orderedVersions[1].Id);
|
||||
Assert.Equal(version1.Id, orderedVersions[2].Id); // Oldest
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.Security.Claims;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.Registration;
|
||||
@@ -10,6 +11,7 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Sso.Controllers;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -1137,4 +1139,129 @@ public class AccountControllerTest
|
||||
Assert.NotNull(result.user);
|
||||
Assert.Equal(email, result.user.Email);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ExternalChallenge_WithMatchingOrgId_Succeeds(
|
||||
SutProvider<AccountController> sutProvider,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
var orgId = organization.Id;
|
||||
var scheme = orgId.ToString();
|
||||
var returnUrl = "~/vault";
|
||||
var state = "test-state";
|
||||
var userIdentifier = "user-123";
|
||||
var ssoToken = "valid-sso-token";
|
||||
|
||||
// Mock the data protector to return a tokenable with matching org ID
|
||||
var dataProtector = sutProvider.GetDependency<IDataProtectorTokenFactory<SsoTokenable>>();
|
||||
var tokenable = new SsoTokenable(organization, 3600);
|
||||
dataProtector.Unprotect(ssoToken).Returns(tokenable);
|
||||
|
||||
// Mock URL helper for IsLocalUrl check
|
||||
var urlHelper = Substitute.For<IUrlHelper>();
|
||||
urlHelper.IsLocalUrl(returnUrl).Returns(true);
|
||||
sutProvider.Sut.Url = urlHelper;
|
||||
|
||||
// Mock interaction service for IsValidReturnUrl check
|
||||
var interactionService = sutProvider.GetDependency<IIdentityServerInteractionService>();
|
||||
interactionService.IsValidReturnUrl(returnUrl).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.ExternalChallenge(scheme, returnUrl, state, userIdentifier, ssoToken);
|
||||
|
||||
// Assert
|
||||
var challengeResult = Assert.IsType<ChallengeResult>(result);
|
||||
Assert.Contains(scheme, challengeResult.AuthenticationSchemes);
|
||||
Assert.NotNull(challengeResult.Properties);
|
||||
Assert.Equal(scheme, challengeResult.Properties.Items["scheme"]);
|
||||
Assert.Equal(returnUrl, challengeResult.Properties.Items["return_url"]);
|
||||
Assert.Equal(state, challengeResult.Properties.Items["state"]);
|
||||
Assert.Equal(userIdentifier, challengeResult.Properties.Items["user_identifier"]);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ExternalChallenge_WithMismatchedOrgId_ThrowsSsoOrganizationIdMismatch(
|
||||
SutProvider<AccountController> sutProvider,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
var correctOrgId = organization.Id;
|
||||
var wrongOrgId = Guid.NewGuid();
|
||||
var scheme = wrongOrgId.ToString(); // Different from tokenable's org ID
|
||||
var returnUrl = "~/vault";
|
||||
var state = "test-state";
|
||||
var userIdentifier = "user-123";
|
||||
var ssoToken = "valid-sso-token";
|
||||
|
||||
// Mock the data protector to return a tokenable with different org ID
|
||||
var dataProtector = sutProvider.GetDependency<IDataProtectorTokenFactory<SsoTokenable>>();
|
||||
var tokenable = new SsoTokenable(organization, 3600); // Contains correctOrgId
|
||||
dataProtector.Unprotect(ssoToken).Returns(tokenable);
|
||||
|
||||
// Mock i18n service to return the key
|
||||
sutProvider.GetDependency<II18nService>()
|
||||
.T(Arg.Any<string>())
|
||||
.Returns(ci => (string)ci[0]!);
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<Exception>(() =>
|
||||
sutProvider.Sut.ExternalChallenge(scheme, returnUrl, state, userIdentifier, ssoToken));
|
||||
Assert.Equal("SsoOrganizationIdMismatch", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ExternalChallenge_WithInvalidSchemeFormat_ThrowsSsoOrganizationIdMismatch(
|
||||
SutProvider<AccountController> sutProvider,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
var scheme = "not-a-valid-guid";
|
||||
var returnUrl = "~/vault";
|
||||
var state = "test-state";
|
||||
var userIdentifier = "user-123";
|
||||
var ssoToken = "valid-sso-token";
|
||||
|
||||
// Mock the data protector to return a valid tokenable
|
||||
var dataProtector = sutProvider.GetDependency<IDataProtectorTokenFactory<SsoTokenable>>();
|
||||
var tokenable = new SsoTokenable(organization, 3600);
|
||||
dataProtector.Unprotect(ssoToken).Returns(tokenable);
|
||||
|
||||
// Mock i18n service to return the key
|
||||
sutProvider.GetDependency<II18nService>()
|
||||
.T(Arg.Any<string>())
|
||||
.Returns(ci => (string)ci[0]!);
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<Exception>(() =>
|
||||
sutProvider.Sut.ExternalChallenge(scheme, returnUrl, state, userIdentifier, ssoToken));
|
||||
Assert.Equal("SsoOrganizationIdMismatch", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ExternalChallenge_WithInvalidSsoToken_ThrowsInvalidSsoToken(
|
||||
SutProvider<AccountController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var orgId = Guid.NewGuid();
|
||||
var scheme = orgId.ToString();
|
||||
var returnUrl = "~/vault";
|
||||
var state = "test-state";
|
||||
var userIdentifier = "user-123";
|
||||
var ssoToken = "invalid-corrupted-token";
|
||||
|
||||
// Mock the data protector to throw when trying to unprotect
|
||||
var dataProtector = sutProvider.GetDependency<IDataProtectorTokenFactory<SsoTokenable>>();
|
||||
dataProtector.Unprotect(ssoToken).Returns(_ => throw new Exception("Token validation failed"));
|
||||
|
||||
// Mock i18n service to return the key
|
||||
sutProvider.GetDependency<II18nService>()
|
||||
.T(Arg.Any<string>())
|
||||
.Returns(ci => (string)ci[0]!);
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<Exception>(() =>
|
||||
sutProvider.Sut.ExternalChallenge(scheme, returnUrl, state, userIdentifier, ssoToken));
|
||||
Assert.Equal("InvalidSsoToken", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +200,38 @@ public class GroupsControllerTests : IClassFixture<ScimApplicationFactory>, IAsy
|
||||
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetList_SearchDisplayNameWithoutOptionalParameters_Success()
|
||||
{
|
||||
string filter = "displayName eq Test Group 2";
|
||||
int? itemsPerPage = null;
|
||||
int? startIndex = null;
|
||||
var expectedResponse = new ScimListResponseModel<ScimGroupResponseModel>
|
||||
{
|
||||
ItemsPerPage = 50, //default value
|
||||
TotalResults = 1,
|
||||
StartIndex = 1, //default value
|
||||
Resources = new List<ScimGroupResponseModel>
|
||||
{
|
||||
new ScimGroupResponseModel
|
||||
{
|
||||
Id = ScimApplicationFactory.TestGroupId2,
|
||||
DisplayName = "Test Group 2",
|
||||
ExternalId = "B",
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaGroup }
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaListResponse }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsGetListAsync(ScimApplicationFactory.TestOrganizationId1, filter, itemsPerPage, startIndex);
|
||||
|
||||
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
||||
|
||||
var responseModel = JsonSerializer.Deserialize<ScimListResponseModel<ScimGroupResponseModel>>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Post_Success()
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Scim.Groups;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
@@ -24,7 +25,7 @@ public class GetGroupsListCommandTests
|
||||
.GetManyByOrganizationIdAsync(organizationId)
|
||||
.Returns(groups);
|
||||
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, null, count, startIndex);
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Count = count, StartIndex = startIndex });
|
||||
|
||||
AssertHelper.AssertPropertyEqual(groups.Skip(startIndex - 1).Take(count).ToList(), result.groupList);
|
||||
AssertHelper.AssertPropertyEqual(groups.Count, result.totalResults);
|
||||
@@ -47,7 +48,7 @@ public class GetGroupsListCommandTests
|
||||
.GetManyByOrganizationIdAsync(organizationId)
|
||||
.Returns(groups);
|
||||
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, filter, null, null);
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Filter = filter });
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);
|
||||
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
|
||||
@@ -67,7 +68,7 @@ public class GetGroupsListCommandTests
|
||||
.GetManyByOrganizationIdAsync(organizationId)
|
||||
.Returns(groups);
|
||||
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, filter, null, null);
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Filter = filter });
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);
|
||||
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
|
||||
@@ -90,7 +91,7 @@ public class GetGroupsListCommandTests
|
||||
.GetManyByOrganizationIdAsync(organizationId)
|
||||
.Returns(groups);
|
||||
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, filter, null, null);
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Filter = filter });
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);
|
||||
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
|
||||
@@ -112,7 +113,7 @@ public class GetGroupsListCommandTests
|
||||
.GetManyByOrganizationIdAsync(organizationId)
|
||||
.Returns(groups);
|
||||
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, filter, null, null);
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Filter = filter });
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);
|
||||
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Users;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
@@ -36,7 +37,7 @@ public class PostUserCommandTests
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IPaymentService>().HasSecretsManagerStandalone(organization).Returns(true);
|
||||
sutProvider.GetDependency<IStripePaymentService>().HasSecretsManagerStandalone(organization).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationService>()
|
||||
.InviteUserAsync(organizationId,
|
||||
|
||||
@@ -18,11 +18,11 @@ if ($LASTEXITCODE -ne 0) {
|
||||
# Api internal & public
|
||||
Set-Location "../../src/Api"
|
||||
dotnet build
|
||||
dotnet swagger tofile --output "../../api.json" --host "https://api.bitwarden.com" "./bin/Debug/net8.0/Api.dll" "internal"
|
||||
dotnet swagger tofile --output "../../api.json" "./bin/Debug/net8.0/Api.dll" "internal"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
dotnet swagger tofile --output "../../api.public.json" --host "https://api.bitwarden.com" "./bin/Debug/net8.0/Api.dll" "public"
|
||||
dotnet swagger tofile --output "../../api.public.json" "./bin/Debug/net8.0/Api.dll" "public"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
"id": "<your Installation Id>",
|
||||
"key": "<your Installation Key>"
|
||||
},
|
||||
"events": {
|
||||
"connectionString": "",
|
||||
"queueName": "event"
|
||||
},
|
||||
"licenseDirectory": "<full path to license directory>",
|
||||
"enableNewDeviceVerification": true,
|
||||
"enableEmailVerification": true
|
||||
|
||||
132
dev/verify_migrations.ps1
Normal file
132
dev/verify_migrations.ps1
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Validates that new database migration files follow naming conventions and chronological order.
|
||||
|
||||
.DESCRIPTION
|
||||
This script validates migration files in util/Migrator/DbScripts/ to ensure:
|
||||
1. New migrations follow the naming format: YYYY-MM-DD_NN_Description.sql
|
||||
2. New migrations are chronologically ordered (filename sorts after existing migrations)
|
||||
3. Dates use leading zeros (e.g., 2025-01-05, not 2025-1-5)
|
||||
4. A 2-digit sequence number is included (e.g., _00, _01)
|
||||
|
||||
.PARAMETER BaseRef
|
||||
The base git reference to compare against (e.g., 'main', 'HEAD~1')
|
||||
|
||||
.PARAMETER CurrentRef
|
||||
The current git reference (defaults to 'HEAD')
|
||||
|
||||
.EXAMPLE
|
||||
# For pull requests - compare against main branch
|
||||
.\verify_migrations.ps1 -BaseRef main
|
||||
|
||||
.EXAMPLE
|
||||
# For pushes - compare against previous commit
|
||||
.\verify_migrations.ps1 -BaseRef HEAD~1
|
||||
#>
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$BaseRef,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$CurrentRef = "HEAD"
|
||||
)
|
||||
|
||||
# Use invariant culture for consistent string comparison
|
||||
[System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::InvariantCulture
|
||||
|
||||
$migrationPath = "util/Migrator/DbScripts"
|
||||
|
||||
# Get list of migrations from base reference
|
||||
try {
|
||||
$baseMigrations = git ls-tree -r --name-only $BaseRef -- "$migrationPath/*.sql" 2>$null | Sort-Object
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Warning: Could not retrieve migrations from base reference '$BaseRef'"
|
||||
$baseMigrations = @()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host "Warning: Could not retrieve migrations from base reference '$BaseRef'"
|
||||
$baseMigrations = @()
|
||||
}
|
||||
|
||||
# Get list of migrations from current reference
|
||||
$currentMigrations = git ls-tree -r --name-only $CurrentRef -- "$migrationPath/*.sql" | Sort-Object
|
||||
|
||||
# Find added migrations
|
||||
$addedMigrations = $currentMigrations | Where-Object { $_ -notin $baseMigrations }
|
||||
|
||||
if ($addedMigrations.Count -eq 0) {
|
||||
Write-Host "No new migration files added."
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "New migration files detected:"
|
||||
$addedMigrations | ForEach-Object { Write-Host " $_" }
|
||||
Write-Host ""
|
||||
|
||||
# Get the last migration from base reference
|
||||
if ($baseMigrations.Count -eq 0) {
|
||||
Write-Host "No previous migrations found (initial commit?). Skipping validation."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$lastBaseMigration = Split-Path -Leaf ($baseMigrations | Select-Object -Last 1)
|
||||
Write-Host "Last migration in base reference: $lastBaseMigration"
|
||||
Write-Host ""
|
||||
|
||||
# Required format regex: YYYY-MM-DD_NN_Description.sql
|
||||
$formatRegex = '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}_.+\.sql$'
|
||||
|
||||
$validationFailed = $false
|
||||
|
||||
foreach ($migration in $addedMigrations) {
|
||||
$migrationName = Split-Path -Leaf $migration
|
||||
|
||||
# Validate NEW migration filename format
|
||||
if ($migrationName -notmatch $formatRegex) {
|
||||
Write-Host "ERROR: Migration '$migrationName' does not match required format"
|
||||
Write-Host "Required format: YYYY-MM-DD_NN_Description.sql"
|
||||
Write-Host " - YYYY: 4-digit year"
|
||||
Write-Host " - MM: 2-digit month with leading zero (01-12)"
|
||||
Write-Host " - DD: 2-digit day with leading zero (01-31)"
|
||||
Write-Host " - NN: 2-digit sequence number (00, 01, 02, etc.)"
|
||||
Write-Host "Example: 2025-01-15_00_MyMigration.sql"
|
||||
$validationFailed = $true
|
||||
continue
|
||||
}
|
||||
|
||||
# Compare migration name with last base migration (using ordinal string comparison)
|
||||
if ([string]::CompareOrdinal($migrationName, $lastBaseMigration) -lt 0) {
|
||||
Write-Host "ERROR: New migration '$migrationName' is not chronologically after '$lastBaseMigration'"
|
||||
$validationFailed = $true
|
||||
}
|
||||
else {
|
||||
Write-Host "OK: '$migrationName' is chronologically after '$lastBaseMigration'"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
if ($validationFailed) {
|
||||
Write-Host "FAILED: One or more migrations are incorrectly named or not in chronological order"
|
||||
Write-Host ""
|
||||
Write-Host "All new migration files must:"
|
||||
Write-Host " 1. Follow the naming format: YYYY-MM-DD_NN_Description.sql"
|
||||
Write-Host " 2. Use leading zeros in dates (e.g., 2025-01-05, not 2025-1-5)"
|
||||
Write-Host " 3. Include a 2-digit sequence number (e.g., _00, _01)"
|
||||
Write-Host " 4. Have a filename that sorts after the last migration in base"
|
||||
Write-Host ""
|
||||
Write-Host "To fix this issue:"
|
||||
Write-Host " 1. Locate your migration file(s) in util/Migrator/DbScripts/"
|
||||
Write-Host " 2. Rename to follow format: YYYY-MM-DD_NN_Description.sql"
|
||||
Write-Host " 3. Ensure the date is after $lastBaseMigration"
|
||||
Write-Host ""
|
||||
Write-Host "Example: 2025-01-15_00_AddNewFeature.sql"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "SUCCESS: All new migrations are correctly named and in chronological order"
|
||||
exit 0
|
||||
@@ -5,6 +5,7 @@
|
||||
},
|
||||
"msbuild-sdks": {
|
||||
"Microsoft.Build.Traversal": "4.1.0",
|
||||
"Microsoft.Build.Sql": "1.0.0"
|
||||
"Microsoft.Build.Sql": "1.0.0",
|
||||
"Bitwarden.Server.Sdk": "1.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@ using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Organizations.Services;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.OrganizationConnectionConfigs;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
@@ -41,7 +43,7 @@ public class OrganizationsController : Controller
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly IStripePaymentService _paymentService;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
@@ -56,6 +58,7 @@ public class OrganizationsController : Controller
|
||||
private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
|
||||
private readonly IOrganizationBillingService _organizationBillingService;
|
||||
|
||||
public OrganizationsController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@@ -66,7 +69,7 @@ public class OrganizationsController : Controller
|
||||
ICollectionRepository collectionRepository,
|
||||
IGroupRepository groupRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
IPaymentService paymentService,
|
||||
IStripePaymentService paymentService,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
GlobalSettings globalSettings,
|
||||
IProviderRepository providerRepository,
|
||||
@@ -80,7 +83,8 @@ public class OrganizationsController : Controller
|
||||
IProviderBillingService providerBillingService,
|
||||
IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand,
|
||||
IPricingClient pricingClient,
|
||||
IResendOrganizationInviteCommand resendOrganizationInviteCommand)
|
||||
IResendOrganizationInviteCommand resendOrganizationInviteCommand,
|
||||
IOrganizationBillingService organizationBillingService)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@@ -105,6 +109,7 @@ public class OrganizationsController : Controller
|
||||
_organizationInitiateDeleteCommand = organizationInitiateDeleteCommand;
|
||||
_pricingClient = pricingClient;
|
||||
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
|
||||
_organizationBillingService = organizationBillingService;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Org_List_View)]
|
||||
@@ -241,6 +246,8 @@ public class OrganizationsController : Controller
|
||||
var existingOrganizationData = new Organization
|
||||
{
|
||||
Id = organization.Id,
|
||||
Name = organization.Name,
|
||||
BillingEmail = organization.BillingEmail,
|
||||
Status = organization.Status,
|
||||
PlanType = organization.PlanType,
|
||||
Seats = organization.Seats
|
||||
@@ -286,6 +293,22 @@ public class OrganizationsController : Controller
|
||||
|
||||
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
// Sync name/email changes to Stripe
|
||||
if (existingOrganizationData.Name != organization.Name || existingOrganizationData.BillingEmail != organization.BillingEmail)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _organizationBillingService.UpdateOrganizationNameAndEmail(organization);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
"Failed to update Stripe customer for organization {OrganizationId}. Database was updated successfully.",
|
||||
organization.Id);
|
||||
TempData["Warning"] = "Organization updated successfully, but Stripe customer name/email synchronization failed.";
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
|
||||
@@ -473,6 +496,7 @@ public class OrganizationsController : Controller
|
||||
organization.UseOrganizationDomains = model.UseOrganizationDomains;
|
||||
organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies;
|
||||
organization.UseAutomaticUserConfirmation = model.UseAutomaticUserConfirmation;
|
||||
organization.UsePhishingBlocker = model.UsePhishingBlocker;
|
||||
|
||||
//secrets
|
||||
organization.SmSeats = model.SmSeats;
|
||||
|
||||
@@ -56,6 +56,7 @@ public class ProvidersController : Controller
|
||||
private readonly IStripeAdapter _stripeAdapter;
|
||||
private readonly IAccessControlService _accessControlService;
|
||||
private readonly ISubscriberService _subscriberService;
|
||||
private readonly ILogger<ProvidersController> _logger;
|
||||
|
||||
public ProvidersController(IOrganizationRepository organizationRepository,
|
||||
IResellerClientOrganizationSignUpCommand resellerClientOrganizationSignUpCommand,
|
||||
@@ -72,7 +73,8 @@ public class ProvidersController : Controller
|
||||
IPricingClient pricingClient,
|
||||
IStripeAdapter stripeAdapter,
|
||||
IAccessControlService accessControlService,
|
||||
ISubscriberService subscriberService)
|
||||
ISubscriberService subscriberService,
|
||||
ILogger<ProvidersController> logger)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_resellerClientOrganizationSignUpCommand = resellerClientOrganizationSignUpCommand;
|
||||
@@ -92,6 +94,7 @@ public class ProvidersController : Controller
|
||||
_braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl();
|
||||
_braintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||
_subscriberService = subscriberService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Provider_List_View)]
|
||||
@@ -296,6 +299,9 @@ public class ProvidersController : Controller
|
||||
|
||||
var originalProviderStatus = provider.Enabled;
|
||||
|
||||
// Capture original billing email before modifications for Stripe sync
|
||||
var originalBillingEmail = provider.BillingEmail;
|
||||
|
||||
model.ToProvider(provider);
|
||||
|
||||
// validate the stripe ids to prevent saving a bad one
|
||||
@@ -321,6 +327,22 @@ public class ProvidersController : Controller
|
||||
await _providerService.UpdateAsync(provider);
|
||||
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
|
||||
|
||||
// Sync billing email changes to Stripe
|
||||
if (!string.IsNullOrEmpty(provider.GatewayCustomerId) && originalBillingEmail != provider.BillingEmail)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _providerBillingService.UpdateProviderNameAndEmail(provider);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
"Failed to update Stripe customer for provider {ProviderId}. Database was updated successfully.",
|
||||
provider.Id);
|
||||
TempData["Warning"] = "Provider updated successfully, but Stripe customer email synchronization failed.";
|
||||
}
|
||||
}
|
||||
|
||||
if (!provider.IsBillable())
|
||||
{
|
||||
return RedirectToAction("Edit", new { id });
|
||||
@@ -339,11 +361,11 @@ public class ProvidersController : Controller
|
||||
]);
|
||||
await _providerBillingService.UpdateSeatMinimums(updateMspSeatMinimumsCommand);
|
||||
|
||||
var customer = await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId);
|
||||
var customer = await _stripeAdapter.GetCustomerAsync(provider.GatewayCustomerId);
|
||||
if (model.PayByInvoice != customer.ApprovedToPayByInvoice())
|
||||
{
|
||||
var approvedToPayByInvoice = model.PayByInvoice ? "1" : "0";
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
await _stripeAdapter.UpdateCustomerAsync(customer.Id, new CustomerUpdateOptions
|
||||
{
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
|
||||
@@ -107,6 +107,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
|
||||
UseOrganizationDomains = org.UseOrganizationDomains;
|
||||
UseAutomaticUserConfirmation = org.UseAutomaticUserConfirmation;
|
||||
UsePhishingBlocker = org.UsePhishingBlocker;
|
||||
|
||||
_plans = plans;
|
||||
}
|
||||
@@ -160,6 +161,8 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
public new bool UseSecretsManager { get; set; }
|
||||
[Display(Name = "Risk Insights")]
|
||||
public new bool UseRiskInsights { get; set; }
|
||||
[Display(Name = "Phishing Blocker")]
|
||||
public new bool UsePhishingBlocker { get; set; }
|
||||
[Display(Name = "Admin Sponsored Families")]
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
[Display(Name = "Self Host")]
|
||||
@@ -327,6 +330,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
existingOrganization.SmServiceAccounts = SmServiceAccounts;
|
||||
existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts;
|
||||
existingOrganization.UseOrganizationDomains = UseOrganizationDomains;
|
||||
existingOrganization.UsePhishingBlocker = UsePhishingBlocker;
|
||||
return existingOrganization;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ public class OrganizationViewModel
|
||||
public int OccupiedSmSeatsCount { get; set; }
|
||||
public bool UseSecretsManager => Organization.UseSecretsManager;
|
||||
public bool UseRiskInsights => Organization.UseRiskInsights;
|
||||
public bool UsePhishingBlocker => Organization.UsePhishingBlocker;
|
||||
public IEnumerable<OrganizationUserUserDetails> OwnersDetails { get; set; }
|
||||
public IEnumerable<OrganizationUserUserDetails> AdminsDetails { get; set; }
|
||||
}
|
||||
|
||||
@@ -156,6 +156,10 @@
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseAdminSponsoredFamilies" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseAdminSponsoredFamilies"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UsePhishingBlocker" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UsePhishingBlocker"></label>
|
||||
</div>
|
||||
@if(FeatureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
|
||||
{
|
||||
<div class="form-check">
|
||||
|
||||
@@ -8,6 +8,7 @@ using Bit.Admin.Utilities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Organizations.Queries;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Platform.Installations;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
@@ -5,6 +5,7 @@ using Bit.Admin.Models;
|
||||
using Bit.Admin.Services;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@@ -20,7 +21,7 @@ public class UsersController : Controller
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ICipherRepository _cipherRepository;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly IStripePaymentService _paymentService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IAccessControlService _accessControlService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
@@ -30,7 +31,7 @@ public class UsersController : Controller
|
||||
public UsersController(
|
||||
IUserRepository userRepository,
|
||||
ICipherRepository cipherRepository,
|
||||
IPaymentService paymentService,
|
||||
IStripePaymentService paymentService,
|
||||
GlobalSettings globalSettings,
|
||||
IAccessControlService accessControlService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
|
||||
@@ -19,7 +19,7 @@ public class DatabaseMigrationHostedService : IHostedService, IDisposable
|
||||
public virtual async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Wait 20 seconds to allow database to come online
|
||||
await Task.Delay(20000);
|
||||
await Task.Delay(20000, cancellationToken);
|
||||
|
||||
var maxMigrationAttempts = 10;
|
||||
for (var i = 1; i <= maxMigrationAttempts; i++)
|
||||
@@ -41,7 +41,7 @@ public class DatabaseMigrationHostedService : IHostedService, IDisposable
|
||||
{
|
||||
_logger.LogError(e,
|
||||
"Database unavailable for migration. Trying again (attempt #{0})...", i + 1);
|
||||
await Task.Delay(20000);
|
||||
await Task.Delay(20000, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -12,8 +12,10 @@ namespace Bit.Api.AdminConsole.Controllers;
|
||||
[Authorize("Application")]
|
||||
public class OrganizationIntegrationConfigurationController(
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationIntegrationRepository integrationRepository,
|
||||
IOrganizationIntegrationConfigurationRepository integrationConfigurationRepository) : Controller
|
||||
ICreateOrganizationIntegrationConfigurationCommand createCommand,
|
||||
IUpdateOrganizationIntegrationConfigurationCommand updateCommand,
|
||||
IDeleteOrganizationIntegrationConfigurationCommand deleteCommand,
|
||||
IGetOrganizationIntegrationConfigurationsQuery getQuery) : Controller
|
||||
{
|
||||
[HttpGet("")]
|
||||
public async Task<List<OrganizationIntegrationConfigurationResponseModel>> GetAsync(
|
||||
@@ -24,13 +26,8 @@ public class OrganizationIntegrationConfigurationController(
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||
if (integration == null || integration.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var configurations = await integrationConfigurationRepository.GetManyByIntegrationAsync(integrationId);
|
||||
var configurations = await getQuery.GetManyByIntegrationAsync(organizationId, integrationId);
|
||||
return configurations
|
||||
.Select(configuration => new OrganizationIntegrationConfigurationResponseModel(configuration))
|
||||
.ToList();
|
||||
@@ -46,19 +43,11 @@ public class OrganizationIntegrationConfigurationController(
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||
if (integration == null || integration.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
if (!model.IsValidForType(integration.Type))
|
||||
{
|
||||
throw new BadRequestException($"Invalid Configuration and/or Template for integration type {integration.Type}");
|
||||
}
|
||||
|
||||
var organizationIntegrationConfiguration = model.ToOrganizationIntegrationConfiguration(integrationId);
|
||||
var configuration = await integrationConfigurationRepository.CreateAsync(organizationIntegrationConfiguration);
|
||||
return new OrganizationIntegrationConfigurationResponseModel(configuration);
|
||||
var configuration = model.ToOrganizationIntegrationConfiguration(integrationId);
|
||||
var created = await createCommand.CreateAsync(organizationId, integrationId, configuration);
|
||||
|
||||
return new OrganizationIntegrationConfigurationResponseModel(created);
|
||||
}
|
||||
|
||||
[HttpPut("{configurationId:guid}")]
|
||||
@@ -72,26 +61,11 @@ public class OrganizationIntegrationConfigurationController(
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||
if (integration == null || integration.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
if (!model.IsValidForType(integration.Type))
|
||||
{
|
||||
throw new BadRequestException($"Invalid Configuration and/or Template for integration type {integration.Type}");
|
||||
}
|
||||
|
||||
var configuration = await integrationConfigurationRepository.GetByIdAsync(configurationId);
|
||||
if (configuration is null || configuration.OrganizationIntegrationId != integrationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
var configuration = model.ToOrganizationIntegrationConfiguration(integrationId);
|
||||
var updated = await updateCommand.UpdateAsync(organizationId, integrationId, configurationId, configuration);
|
||||
|
||||
var newConfiguration = model.ToOrganizationIntegrationConfiguration(configuration);
|
||||
await integrationConfigurationRepository.ReplaceAsync(newConfiguration);
|
||||
|
||||
return new OrganizationIntegrationConfigurationResponseModel(newConfiguration);
|
||||
return new OrganizationIntegrationConfigurationResponseModel(updated);
|
||||
}
|
||||
|
||||
[HttpDelete("{configurationId:guid}")]
|
||||
@@ -101,19 +75,8 @@ public class OrganizationIntegrationConfigurationController(
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||
if (integration == null || integration.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var configuration = await integrationConfigurationRepository.GetByIdAsync(configurationId);
|
||||
if (configuration is null || configuration.OrganizationIntegrationId != integrationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await integrationConfigurationRepository.DeleteAsync(configuration);
|
||||
await deleteCommand.DeleteAsync(organizationId, integrationId, configurationId);
|
||||
}
|
||||
|
||||
[HttpPost("{configurationId:guid}/delete")]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -12,7 +12,10 @@ namespace Bit.Api.AdminConsole.Controllers;
|
||||
[Authorize("Application")]
|
||||
public class OrganizationIntegrationController(
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationIntegrationRepository integrationRepository) : Controller
|
||||
ICreateOrganizationIntegrationCommand createCommand,
|
||||
IUpdateOrganizationIntegrationCommand updateCommand,
|
||||
IDeleteOrganizationIntegrationCommand deleteCommand,
|
||||
IGetOrganizationIntegrationsQuery getQuery) : Controller
|
||||
{
|
||||
[HttpGet("")]
|
||||
public async Task<List<OrganizationIntegrationResponseModel>> GetAsync(Guid organizationId)
|
||||
@@ -22,7 +25,7 @@ public class OrganizationIntegrationController(
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var integrations = await integrationRepository.GetManyByOrganizationAsync(organizationId);
|
||||
var integrations = await getQuery.GetManyByOrganizationAsync(organizationId);
|
||||
return integrations
|
||||
.Select(integration => new OrganizationIntegrationResponseModel(integration))
|
||||
.ToList();
|
||||
@@ -36,8 +39,10 @@ public class OrganizationIntegrationController(
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var integration = await integrationRepository.CreateAsync(model.ToOrganizationIntegration(organizationId));
|
||||
return new OrganizationIntegrationResponseModel(integration);
|
||||
var integration = model.ToOrganizationIntegration(organizationId);
|
||||
var created = await createCommand.CreateAsync(integration);
|
||||
|
||||
return new OrganizationIntegrationResponseModel(created);
|
||||
}
|
||||
|
||||
[HttpPut("{integrationId:guid}")]
|
||||
@@ -48,14 +53,10 @@ public class OrganizationIntegrationController(
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||
if (integration is null || integration.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
var integration = model.ToOrganizationIntegration(organizationId);
|
||||
var updated = await updateCommand.UpdateAsync(organizationId, integrationId, integration);
|
||||
|
||||
await integrationRepository.ReplaceAsync(model.ToOrganizationIntegration(integration));
|
||||
return new OrganizationIntegrationResponseModel(integration);
|
||||
return new OrganizationIntegrationResponseModel(updated);
|
||||
}
|
||||
|
||||
[HttpDelete("{integrationId:guid}")]
|
||||
@@ -66,13 +67,7 @@ public class OrganizationIntegrationController(
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||
if (integration is null || integration.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await integrationRepository.DeleteAsync(integration);
|
||||
await deleteCommand.DeleteAsync(organizationId, integrationId);
|
||||
}
|
||||
|
||||
[HttpPost("{integrationId:guid}/delete")]
|
||||
|
||||
@@ -41,6 +41,8 @@ using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using V1_RevokeOrganizationUserCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1.IRevokeOrganizationUserCommand;
|
||||
using V2_RevokeOrganizationUserCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Controllers;
|
||||
|
||||
@@ -71,11 +73,13 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
|
||||
private readonly IBulkResendOrganizationInvitesCommand _bulkResendOrganizationInvitesCommand;
|
||||
private readonly IAutomaticallyConfirmOrganizationUserCommand _automaticallyConfirmOrganizationUserCommand;
|
||||
private readonly V2_RevokeOrganizationUserCommand.IRevokeOrganizationUserCommand _revokeOrganizationUserCommandVNext;
|
||||
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
|
||||
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||
private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand;
|
||||
private readonly IRevokeOrganizationUserCommand _revokeOrganizationUserCommand;
|
||||
private readonly V1_RevokeOrganizationUserCommand _revokeOrganizationUserCommand;
|
||||
private readonly IAdminRecoverAccountCommand _adminRecoverAccountCommand;
|
||||
|
||||
public OrganizationUsersController(IOrganizationRepository organizationRepository,
|
||||
@@ -103,10 +107,12 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
IConfirmOrganizationUserCommand confirmOrganizationUserCommand,
|
||||
IRestoreOrganizationUserCommand restoreOrganizationUserCommand,
|
||||
IInitPendingOrganizationCommand initPendingOrganizationCommand,
|
||||
IRevokeOrganizationUserCommand revokeOrganizationUserCommand,
|
||||
V1_RevokeOrganizationUserCommand revokeOrganizationUserCommand,
|
||||
IResendOrganizationInviteCommand resendOrganizationInviteCommand,
|
||||
IBulkResendOrganizationInvitesCommand bulkResendOrganizationInvitesCommand,
|
||||
IAdminRecoverAccountCommand adminRecoverAccountCommand,
|
||||
IAutomaticallyConfirmOrganizationUserCommand automaticallyConfirmOrganizationUserCommand)
|
||||
IAutomaticallyConfirmOrganizationUserCommand automaticallyConfirmOrganizationUserCommand,
|
||||
V2_RevokeOrganizationUserCommand.IRevokeOrganizationUserCommand revokeOrganizationUserCommandVNext)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@@ -131,7 +137,9 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
_featureService = featureService;
|
||||
_pricingClient = pricingClient;
|
||||
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
|
||||
_bulkResendOrganizationInvitesCommand = bulkResendOrganizationInvitesCommand;
|
||||
_automaticallyConfirmOrganizationUserCommand = automaticallyConfirmOrganizationUserCommand;
|
||||
_revokeOrganizationUserCommandVNext = revokeOrganizationUserCommandVNext;
|
||||
_confirmOrganizationUserCommand = confirmOrganizationUserCommand;
|
||||
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||
_initPendingOrganizationCommand = initPendingOrganizationCommand;
|
||||
@@ -273,7 +281,17 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkReinvite(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
var result = await _organizationService.ResendInvitesAsync(orgId, userId.Value, model.Ids);
|
||||
|
||||
IEnumerable<Tuple<Core.Entities.OrganizationUser, string>> result;
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.IncreaseBulkReinviteLimitForCloud))
|
||||
{
|
||||
result = await _bulkResendOrganizationInvitesCommand.BulkResendInvitesAsync(orgId, userId.Value, model.Ids);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = await _organizationService.ResendInvitesAsync(orgId, userId.Value, model.Ids);
|
||||
}
|
||||
|
||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(
|
||||
result.Select(t => new OrganizationUserBulkResponseModel(t.Item1.Id, t.Item2)));
|
||||
}
|
||||
@@ -483,43 +501,10 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
}
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
[HttpPut("{id}/reset-password")]
|
||||
[Authorize<ManageAccountRecoveryRequirement>]
|
||||
public async Task<IResult> PutResetPassword(Guid orgId, Guid id, [FromBody] OrganizationUserResetPasswordRequestModel model)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountRecoveryCommand))
|
||||
{
|
||||
// TODO: remove legacy implementation after feature flag is enabled.
|
||||
return await PutResetPasswordNew(orgId, id, model);
|
||||
}
|
||||
|
||||
// Get the users role, since provider users aren't a member of the organization we use the owner check
|
||||
var orgUserType = await _currentContext.OrganizationOwner(orgId)
|
||||
? OrganizationUserType.Owner
|
||||
: _currentContext.Organizations?.FirstOrDefault(o => o.Id == orgId)?.Type;
|
||||
if (orgUserType == null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var result = await _userService.AdminResetPasswordAsync(orgUserType.Value, orgId, id, model.NewMasterPasswordHash, model.Key);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
|
||||
await Task.Delay(2000);
|
||||
return TypedResults.BadRequest(ModelState);
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
// TODO: make sure the route and authorize attributes are maintained when the legacy implementation is removed.
|
||||
private async Task<IResult> PutResetPasswordNew(Guid orgId, Guid id, [FromBody] OrganizationUserResetPasswordRequestModel model)
|
||||
{
|
||||
var targetOrganizationUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||
if (targetOrganizationUser == null || targetOrganizationUser.OrganizationId != orgId)
|
||||
@@ -662,7 +647,29 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRevokeAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||
{
|
||||
return await RestoreOrRevokeUsersAsync(orgId, model, _revokeOrganizationUserCommand.RevokeUsersAsync);
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.BulkRevokeUsersV2))
|
||||
{
|
||||
return await RestoreOrRevokeUsersAsync(orgId, model, _revokeOrganizationUserCommand.RevokeUsersAsync);
|
||||
}
|
||||
|
||||
var currentUserId = _userService.GetProperUserId(User);
|
||||
if (currentUserId == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var results = await _revokeOrganizationUserCommandVNext.RevokeUsersAsync(
|
||||
new V2_RevokeOrganizationUserCommand.RevokeOrganizationUsersRequest(
|
||||
orgId,
|
||||
model.Ids.ToArray(),
|
||||
new StandardUser(currentUserId.Value, await _currentContext.OrganizationOwner(orgId))));
|
||||
|
||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(results
|
||||
.Select(result => new OrganizationUserBulkResponseModel(result.Id,
|
||||
result.Result.Match(
|
||||
error => error.Message,
|
||||
_ => string.Empty
|
||||
))));
|
||||
}
|
||||
|
||||
[HttpPatch("revoke")]
|
||||
|
||||
@@ -42,7 +42,6 @@ public class PoliciesController : Controller
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISavePolicyCommand _savePolicyCommand;
|
||||
private readonly IVNextSavePolicyCommand _vNextSavePolicyCommand;
|
||||
|
||||
@@ -55,7 +54,6 @@ public class PoliciesController : Controller
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IFeatureService featureService,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IVNextSavePolicyCommand vNextSavePolicyCommand)
|
||||
{
|
||||
@@ -69,7 +67,6 @@ public class PoliciesController : Controller
|
||||
_organizationRepository = organizationRepository;
|
||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||
_featureService = featureService;
|
||||
_savePolicyCommand = savePolicyCommand;
|
||||
_vNextSavePolicyCommand = vNextSavePolicyCommand;
|
||||
}
|
||||
@@ -221,9 +218,7 @@ public class PoliciesController : Controller
|
||||
{
|
||||
var savePolicyRequest = await model.ToSavePolicyModelAsync(orgId, type, _currentContext);
|
||||
|
||||
var policy = _featureService.IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor) ?
|
||||
await _vNextSavePolicyCommand.SaveAsync(savePolicyRequest) :
|
||||
await _savePolicyCommand.VNextSaveAsync(savePolicyRequest);
|
||||
var policy = await _vNextSavePolicyCommand.SaveAsync(savePolicyRequest);
|
||||
|
||||
return new PolicyResponseModel(policy);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Bit.Api.AdminConsole.Models.Request.Providers;
|
||||
using Bit.Api.AdminConsole.Models.Response.Providers;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
@@ -23,15 +24,20 @@ public class ProvidersController : Controller
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly ILogger<ProvidersController> _logger;
|
||||
|
||||
public ProvidersController(IUserService userService, IProviderRepository providerRepository,
|
||||
IProviderService providerService, ICurrentContext currentContext, GlobalSettings globalSettings)
|
||||
IProviderService providerService, ICurrentContext currentContext, GlobalSettings globalSettings,
|
||||
IProviderBillingService providerBillingService, ILogger<ProvidersController> logger)
|
||||
{
|
||||
_userService = userService;
|
||||
_providerRepository = providerRepository;
|
||||
_providerService = providerService;
|
||||
_currentContext = currentContext;
|
||||
_globalSettings = globalSettings;
|
||||
_providerBillingService = providerBillingService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
@@ -65,7 +71,27 @@ public class ProvidersController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Capture original values before modifications for Stripe sync
|
||||
var originalName = provider.Name;
|
||||
var originalBillingEmail = provider.BillingEmail;
|
||||
|
||||
await _providerService.UpdateAsync(model.ToProvider(provider, _globalSettings));
|
||||
|
||||
// Sync name/email changes to Stripe
|
||||
if (originalName != provider.Name || originalBillingEmail != provider.BillingEmail)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _providerBillingService.UpdateProviderNameAndEmail(provider);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
"Failed to update Stripe customer for provider {ProviderId}. Database was updated successfully.",
|
||||
provider.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return new ProviderResponseModel(provider);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
|
||||
@@ -16,38 +14,6 @@ public class OrganizationIntegrationConfigurationRequestModel
|
||||
|
||||
public string? Template { get; set; }
|
||||
|
||||
public bool IsValidForType(IntegrationType integrationType)
|
||||
{
|
||||
switch (integrationType)
|
||||
{
|
||||
case IntegrationType.CloudBillingSync or IntegrationType.Scim:
|
||||
return false;
|
||||
case IntegrationType.Slack:
|
||||
return !string.IsNullOrWhiteSpace(Template) &&
|
||||
IsConfigurationValid<SlackIntegrationConfiguration>() &&
|
||||
IsFiltersValid();
|
||||
case IntegrationType.Webhook:
|
||||
return !string.IsNullOrWhiteSpace(Template) &&
|
||||
IsConfigurationValid<WebhookIntegrationConfiguration>() &&
|
||||
IsFiltersValid();
|
||||
case IntegrationType.Hec:
|
||||
return !string.IsNullOrWhiteSpace(Template) &&
|
||||
Configuration is null &&
|
||||
IsFiltersValid();
|
||||
case IntegrationType.Datadog:
|
||||
return !string.IsNullOrWhiteSpace(Template) &&
|
||||
Configuration is null &&
|
||||
IsFiltersValid();
|
||||
case IntegrationType.Teams:
|
||||
return !string.IsNullOrWhiteSpace(Template) &&
|
||||
Configuration is null &&
|
||||
IsFiltersValid();
|
||||
default:
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public OrganizationIntegrationConfiguration ToOrganizationIntegrationConfiguration(Guid organizationIntegrationId)
|
||||
{
|
||||
return new OrganizationIntegrationConfiguration()
|
||||
@@ -59,50 +25,4 @@ public class OrganizationIntegrationConfigurationRequestModel
|
||||
Template = Template
|
||||
};
|
||||
}
|
||||
|
||||
public OrganizationIntegrationConfiguration ToOrganizationIntegrationConfiguration(OrganizationIntegrationConfiguration currentConfiguration)
|
||||
{
|
||||
currentConfiguration.Configuration = Configuration;
|
||||
currentConfiguration.EventType = EventType;
|
||||
currentConfiguration.Filters = Filters;
|
||||
currentConfiguration.Template = Template;
|
||||
|
||||
return currentConfiguration;
|
||||
}
|
||||
|
||||
private bool IsConfigurationValid<T>()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Configuration))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = JsonSerializer.Deserialize<T>(Configuration);
|
||||
return config is not null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsFiltersValid()
|
||||
{
|
||||
if (Filters is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var filters = JsonSerializer.Deserialize<IntegrationFilterGroup>(Filters);
|
||||
return filters is not null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ public class OrganizationUserResetPasswordEnrollmentRequestModel
|
||||
|
||||
public class OrganizationUserBulkRequestModel
|
||||
{
|
||||
[Required]
|
||||
[Required, MinLength(1)]
|
||||
public IEnumerable<Guid> Ids { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ public abstract class BaseProfileOrganizationResponseModel : ResponseModel
|
||||
UseAdminSponsoredFamilies = organizationDetails.UseAdminSponsoredFamilies;
|
||||
UseAutomaticUserConfirmation = organizationDetails.UseAutomaticUserConfirmation;
|
||||
UseSecretsManager = organizationDetails.UseSecretsManager;
|
||||
UsePhishingBlocker = organizationDetails.UsePhishingBlocker;
|
||||
UsePasswordManager = organizationDetails.UsePasswordManager;
|
||||
SelfHost = organizationDetails.SelfHost;
|
||||
Seats = organizationDetails.Seats;
|
||||
@@ -99,6 +100,7 @@ public abstract class BaseProfileOrganizationResponseModel : ResponseModel
|
||||
public bool UseOrganizationDomains { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public bool UseAutomaticUserConfirmation { get; set; }
|
||||
public bool UsePhishingBlocker { get; set; }
|
||||
public bool SelfHost { get; set; }
|
||||
public int? Seats { get; set; }
|
||||
public short? MaxCollections { get; set; }
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Licenses;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Business;
|
||||
@@ -71,6 +74,7 @@ public class OrganizationResponseModel : ResponseModel
|
||||
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation;
|
||||
UsePhishingBlocker = organization.UsePhishingBlocker;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
@@ -120,6 +124,7 @@ public class OrganizationResponseModel : ResponseModel
|
||||
public bool UseOrganizationDomains { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public bool UseAutomaticUserConfirmation { get; set; }
|
||||
public bool UsePhishingBlocker { get; set; }
|
||||
}
|
||||
|
||||
public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
||||
@@ -175,6 +180,30 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
||||
}
|
||||
}
|
||||
|
||||
public OrganizationSubscriptionResponseModel(Organization organization, OrganizationLicense license, ClaimsPrincipal claimsPrincipal) :
|
||||
this(organization, (Plan)null)
|
||||
{
|
||||
if (license != null)
|
||||
{
|
||||
// CRITICAL: When a license has a Token (JWT), ALWAYS use the expiration from the token claim
|
||||
// The token's expiration is cryptographically secured and cannot be tampered with
|
||||
// The file's Expires property can be manually edited and should NOT be trusted for display
|
||||
if (claimsPrincipal != null)
|
||||
{
|
||||
Expiration = claimsPrincipal.GetValue<DateTime>(OrganizationLicenseConstants.Expires);
|
||||
ExpirationWithoutGracePeriod = claimsPrincipal.GetValue<DateTime?>(OrganizationLicenseConstants.ExpirationWithoutGracePeriod);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No token - use the license file expiration (for older licenses without tokens)
|
||||
Expiration = license.Expires;
|
||||
ExpirationWithoutGracePeriod = license.ExpirationWithoutGracePeriod ?? (license.Trial
|
||||
? license.Expires
|
||||
: license.Expires?.AddDays(-Constants.OrganizationSelfHostSubscriptionGracePeriodDays));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string StorageName { get; set; }
|
||||
public double? StorageGb { get; set; }
|
||||
public BillingCustomerDiscount CustomerDiscount { get; set; }
|
||||
|
||||
@@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@@ -24,7 +25,7 @@ public class MembersController : Controller
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IUpdateOrganizationUserCommand _updateOrganizationUserCommand;
|
||||
private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly IStripePaymentService _paymentService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
@@ -37,7 +38,7 @@ public class MembersController : Controller
|
||||
ICurrentContext currentContext,
|
||||
IUpdateOrganizationUserCommand updateOrganizationUserCommand,
|
||||
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand,
|
||||
IPaymentService paymentService,
|
||||
IStripePaymentService paymentService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
|
||||
@@ -5,15 +5,10 @@ using System.Net;
|
||||
using Bit.Api.AdminConsole.Public.Models.Request;
|
||||
using Bit.Api.AdminConsole.Public.Models.Response;
|
||||
using Bit.Api.Models.Public.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -24,25 +19,16 @@ namespace Bit.Api.AdminConsole.Public.Controllers;
|
||||
public class PoliciesController : Controller
|
||||
{
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISavePolicyCommand _savePolicyCommand;
|
||||
private readonly IVNextSavePolicyCommand _vNextSavePolicyCommand;
|
||||
|
||||
public PoliciesController(
|
||||
IPolicyRepository policyRepository,
|
||||
IPolicyService policyService,
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IVNextSavePolicyCommand vNextSavePolicyCommand)
|
||||
{
|
||||
_policyRepository = policyRepository;
|
||||
_policyService = policyService;
|
||||
_currentContext = currentContext;
|
||||
_featureService = featureService;
|
||||
_savePolicyCommand = savePolicyCommand;
|
||||
_vNextSavePolicyCommand = vNextSavePolicyCommand;
|
||||
}
|
||||
|
||||
@@ -97,17 +83,8 @@ public class PoliciesController : Controller
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> Put(PolicyType type, [FromBody] PolicyUpdateRequestModel model)
|
||||
{
|
||||
Policy policy;
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor))
|
||||
{
|
||||
var savePolicyModel = model.ToSavePolicyModel(_currentContext.OrganizationId!.Value, type);
|
||||
policy = await _vNextSavePolicyCommand.SaveAsync(savePolicyModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
var policyUpdate = model.ToPolicyUpdate(_currentContext.OrganizationId!.Value, type);
|
||||
policy = await _savePolicyCommand.SaveAsync(policyUpdate);
|
||||
}
|
||||
var savePolicyModel = model.ToSavePolicyModel(_currentContext.OrganizationId!.Value, type);
|
||||
var policy = await _vNextSavePolicyCommand.SaveAsync(savePolicyModel);
|
||||
|
||||
var response = new PolicyResponseModel(policy);
|
||||
return new JsonResult(response);
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" />
|
||||
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.25.0" />
|
||||
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.31.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Kdf;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Models.Api.Response;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -44,6 +45,7 @@ public class AccountsController : Controller
|
||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||
private readonly ITwoFactorEmailService _twoFactorEmailService;
|
||||
private readonly IChangeKdfCommand _changeKdfCommand;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public AccountsController(
|
||||
IOrganizationService organizationService,
|
||||
@@ -57,7 +59,8 @@ public class AccountsController : Controller
|
||||
IFeatureService featureService,
|
||||
IUserAccountKeysQuery userAccountKeysQuery,
|
||||
ITwoFactorEmailService twoFactorEmailService,
|
||||
IChangeKdfCommand changeKdfCommand
|
||||
IChangeKdfCommand changeKdfCommand,
|
||||
IUserRepository userRepository
|
||||
)
|
||||
{
|
||||
_organizationService = organizationService;
|
||||
@@ -72,6 +75,7 @@ public class AccountsController : Controller
|
||||
_userAccountKeysQuery = userAccountKeysQuery;
|
||||
_twoFactorEmailService = twoFactorEmailService;
|
||||
_changeKdfCommand = changeKdfCommand;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
|
||||
@@ -432,16 +436,36 @@ public class AccountsController : Controller
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.ReturnErrorOnExistingKeypair))
|
||||
if (!string.IsNullOrWhiteSpace(user.PrivateKey) || !string.IsNullOrWhiteSpace(user.PublicKey))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(user.PrivateKey) || !string.IsNullOrWhiteSpace(user.PublicKey))
|
||||
{
|
||||
throw new BadRequestException("User has existing keypair");
|
||||
}
|
||||
throw new BadRequestException("User has existing keypair");
|
||||
}
|
||||
|
||||
if (model.AccountKeys != null)
|
||||
{
|
||||
var accountKeysData = model.AccountKeys.ToAccountKeysData();
|
||||
if (!accountKeysData.IsV2Encryption())
|
||||
{
|
||||
throw new BadRequestException("AccountKeys are only supported for V2 encryption.");
|
||||
}
|
||||
await _userRepository.SetV2AccountCryptographicStateAsync(user.Id, accountKeysData);
|
||||
return new KeysResponseModel(accountKeysData, user.Key);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Todo: Drop this after a transition period. This will drop no-account-keys requests.
|
||||
// The V1 check in the other branch should persist
|
||||
// https://bitwarden.atlassian.net/browse/PM-27329
|
||||
await _userService.SaveUserAsync(model.ToUser(user));
|
||||
return new KeysResponseModel(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||
user.PrivateKey,
|
||||
user.PublicKey
|
||||
)
|
||||
}, user.Key);
|
||||
}
|
||||
|
||||
await _userService.SaveUserAsync(model.ToUser(user));
|
||||
return new KeysResponseModel(user);
|
||||
}
|
||||
|
||||
[HttpGet("keys")]
|
||||
@@ -453,7 +477,8 @@ public class AccountsController : Controller
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
return new KeysResponseModel(user);
|
||||
var accountKeys = await _userAccountKeysQuery.Run(user);
|
||||
return new KeysResponseModel(accountKeys, user.Key);
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
|
||||
@@ -9,7 +9,6 @@ using Bit.Api.Models.Response;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Context;
|
||||
@@ -35,7 +34,7 @@ public class TwoFactorController : Controller
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly UserManager<User> _userManager;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IVerifyAuthRequestCommand _verifyAuthRequestCommand;
|
||||
private readonly IAuthRequestRepository _authRequestRepository;
|
||||
private readonly IDuoUniversalTokenService _duoUniversalTokenService;
|
||||
private readonly IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> _twoFactorAuthenticatorDataProtector;
|
||||
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmailTwoFactorSessionDataProtector;
|
||||
@@ -47,7 +46,7 @@ public class TwoFactorController : Controller
|
||||
IOrganizationService organizationService,
|
||||
UserManager<User> userManager,
|
||||
ICurrentContext currentContext,
|
||||
IVerifyAuthRequestCommand verifyAuthRequestCommand,
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
IDuoUniversalTokenService duoUniversalConfigService,
|
||||
IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> twoFactorAuthenticatorDataProtector,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmailTwoFactorSessionDataProtector,
|
||||
@@ -58,7 +57,7 @@ public class TwoFactorController : Controller
|
||||
_organizationService = organizationService;
|
||||
_userManager = userManager;
|
||||
_currentContext = currentContext;
|
||||
_verifyAuthRequestCommand = verifyAuthRequestCommand;
|
||||
_authRequestRepository = authRequestRepository;
|
||||
_duoUniversalTokenService = duoUniversalConfigService;
|
||||
_twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector;
|
||||
_ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector;
|
||||
@@ -350,14 +349,15 @@ public class TwoFactorController : Controller
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
// Check if 2FA email is from Passwordless.
|
||||
// Check if 2FA email is from a device approval ("Log in with device") scenario.
|
||||
if (!string.IsNullOrEmpty(requestModel.AuthRequestAccessCode))
|
||||
{
|
||||
if (await _verifyAuthRequestCommand
|
||||
.VerifyAuthRequestAsync(new Guid(requestModel.AuthRequestId),
|
||||
requestModel.AuthRequestAccessCode))
|
||||
var authRequest = await _authRequestRepository.GetByIdAsync(new Guid(requestModel.AuthRequestId));
|
||||
if (authRequest != null &&
|
||||
authRequest.IsValidForAuthentication(user.Id, requestModel.AuthRequestAccessCode))
|
||||
{
|
||||
await _twoFactorEmailService.SendTwoFactorEmailAsync(user);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(requestModel.SsoEmail2FaSessionToken))
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#nullable enable
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -12,10 +10,11 @@ namespace Bit.Api.Billing.Controllers;
|
||||
[Route("accounts/billing")]
|
||||
[Authorize("Application")]
|
||||
public class AccountsBillingController(
|
||||
IPaymentService paymentService,
|
||||
IStripePaymentService paymentService,
|
||||
IUserService userService,
|
||||
IPaymentHistoryService paymentHistoryService) : Controller
|
||||
{
|
||||
// TODO: Migrate to Query / AccountBillingVNextController
|
||||
[HttpGet("history")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<BillingHistoryResponseModel> GetBillingHistoryAsync()
|
||||
@@ -30,20 +29,7 @@ public class AccountsBillingController(
|
||||
return new BillingHistoryResponseModel(billingInfo);
|
||||
}
|
||||
|
||||
[HttpGet("payment-method")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<BillingPaymentResponseModel> GetPaymentMethodAsync()
|
||||
{
|
||||
var user = await userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var billingInfo = await paymentService.GetBillingAsync(user);
|
||||
return new BillingPaymentResponseModel(billingInfo);
|
||||
}
|
||||
|
||||
// TODO: Migrate to Query / AccountBillingVNextController
|
||||
[HttpGet("invoices")]
|
||||
public async Task<IResult> GetInvoicesAsync([FromQuery] string? status = null, [FromQuery] string? startAfter = null)
|
||||
{
|
||||
@@ -62,6 +48,7 @@ public class AccountsBillingController(
|
||||
return TypedResults.Ok(invoices);
|
||||
}
|
||||
|
||||
// TODO: Migrate to Query / AccountBillingVNextController
|
||||
[HttpGet("transactions")]
|
||||
public async Task<IResult> GetTransactionsAsync([FromQuery] DateTime? startAfter = null)
|
||||
{
|
||||
@@ -78,18 +65,4 @@ public class AccountsBillingController(
|
||||
|
||||
return TypedResults.Ok(transactions);
|
||||
}
|
||||
|
||||
[HttpPost("preview-invoice")]
|
||||
public async Task<IResult> PreviewInvoiceAsync([FromBody] PreviewIndividualInvoiceRequestBody model)
|
||||
{
|
||||
var user = await userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var invoice = await paymentService.PreviewInvoiceAsync(model, user.GatewayCustomerId, user.GatewaySubscriptionId);
|
||||
|
||||
return TypedResults.Ok(invoice);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Request.Accounts;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
@@ -26,8 +24,10 @@ public class AccountsController(
|
||||
IUserService userService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IUserAccountKeysQuery userAccountKeysQuery,
|
||||
IFeatureService featureService) : Controller
|
||||
IFeatureService featureService,
|
||||
ILicensingService licensingService) : Controller
|
||||
{
|
||||
// TODO: Remove when pm-24996-implement-upgrade-from-free-dialog is removed
|
||||
[HttpPost("premium")]
|
||||
public async Task<PaymentResponseModel> PostPremiumAsync(
|
||||
PremiumRequestModel model,
|
||||
@@ -75,10 +75,11 @@ public class AccountsController(
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Migrate to Query / AccountBillingVNextController as part of Premium -> Organization upgrade work.
|
||||
[HttpGet("subscription")]
|
||||
public async Task<SubscriptionResponseModel> GetSubscriptionAsync(
|
||||
[FromServices] GlobalSettings globalSettings,
|
||||
[FromServices] IPaymentService paymentService)
|
||||
[FromServices] IStripePaymentService paymentService)
|
||||
{
|
||||
var user = await userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
@@ -97,12 +98,14 @@ public class AccountsController(
|
||||
var includeMilestone2Discount = featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2);
|
||||
var subscriptionInfo = await paymentService.GetSubscriptionAsync(user);
|
||||
var license = await userService.GenerateLicenseAsync(user, subscriptionInfo);
|
||||
return new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount);
|
||||
var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(license);
|
||||
return new SubscriptionResponseModel(user, subscriptionInfo, license, claimsPrincipal, includeMilestone2Discount);
|
||||
}
|
||||
else
|
||||
{
|
||||
var license = await userService.GenerateLicenseAsync(user);
|
||||
return new SubscriptionResponseModel(user, license);
|
||||
var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(license);
|
||||
return new SubscriptionResponseModel(user, null, license, claimsPrincipal);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -111,29 +114,7 @@ public class AccountsController(
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("payment")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostPaymentAsync([FromBody] PaymentRequestModel model)
|
||||
{
|
||||
var user = await userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
await userService.ReplacePaymentMethodAsync(user, model.PaymentToken, model.PaymentMethodType!.Value,
|
||||
new TaxInfo
|
||||
{
|
||||
BillingAddressLine1 = model.Line1,
|
||||
BillingAddressLine2 = model.Line2,
|
||||
BillingAddressCity = model.City,
|
||||
BillingAddressState = model.State,
|
||||
BillingAddressCountry = model.Country,
|
||||
BillingAddressPostalCode = model.PostalCode,
|
||||
TaxIdNumber = model.TaxId
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Migrate to Command / AccountBillingVNextController as PUT /account/billing/vnext/subscription
|
||||
[HttpPost("storage")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<PaymentResponseModel> PostStorageAsync([FromBody] StorageRequestModel model)
|
||||
@@ -148,8 +129,11 @@ public class AccountsController(
|
||||
return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result };
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* TODO: A new version of this exists in the AccountBillingVNextController.
|
||||
* The individual-self-hosting-license-uploader.component needs to be updated to use it.
|
||||
* Then, this can be removed.
|
||||
*/
|
||||
[HttpPost("license")]
|
||||
[SelfHosted(SelfHostedOnly = true)]
|
||||
public async Task PostLicenseAsync(LicenseRequestModel model)
|
||||
@@ -169,6 +153,7 @@ public class AccountsController(
|
||||
await userService.UpdateLicenseAsync(user, license);
|
||||
}
|
||||
|
||||
// TODO: Migrate to Command / AccountBillingVNextController as DELETE /account/billing/vnext/subscription
|
||||
[HttpPost("cancel")]
|
||||
public async Task PostCancelAsync(
|
||||
[FromBody] SubscriptionCancellationRequestModel request,
|
||||
@@ -186,6 +171,7 @@ public class AccountsController(
|
||||
user.IsExpired());
|
||||
}
|
||||
|
||||
// TODO: Migrate to Command / AccountBillingVNextController as POST /account/billing/vnext/subscription/reinstate
|
||||
[HttpPost("reinstate-premium")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostReinstateAsync()
|
||||
@@ -199,41 +185,6 @@ public class AccountsController(
|
||||
await userService.ReinstatePremiumAsync(user);
|
||||
}
|
||||
|
||||
[HttpGet("tax")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<TaxInfoResponseModel> GetTaxInfoAsync(
|
||||
[FromServices] IPaymentService paymentService)
|
||||
{
|
||||
var user = await userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var taxInfo = await paymentService.GetTaxInfoAsync(user);
|
||||
return new TaxInfoResponseModel(taxInfo);
|
||||
}
|
||||
|
||||
[HttpPut("tax")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PutTaxInfoAsync(
|
||||
[FromBody] TaxInfoUpdateRequestModel model,
|
||||
[FromServices] IPaymentService paymentService)
|
||||
{
|
||||
var user = await userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var taxInfo = new TaxInfo
|
||||
{
|
||||
BillingAddressPostalCode = model.PostalCode,
|
||||
BillingAddressCountry = model.Country,
|
||||
};
|
||||
await paymentService.SaveTaxInfoAsync(user, taxInfo);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<Guid>> GetOrganizationIdsClaimingUserAsync(Guid userId)
|
||||
{
|
||||
var organizationsClaimingUser = await userService.GetOrganizationsClaimingUserAsync(userId);
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Billing.Controllers;
|
||||
|
||||
[Route("invoices")]
|
||||
[Authorize("Application")]
|
||||
public class InvoicesController : BaseBillingController
|
||||
{
|
||||
[HttpPost("preview-organization")]
|
||||
public async Task<IResult> PreviewInvoiceAsync(
|
||||
[FromBody] PreviewOrganizationInvoiceRequestBody model,
|
||||
[FromServices] ICurrentContext currentContext,
|
||||
[FromServices] IOrganizationRepository organizationRepository,
|
||||
[FromServices] IPaymentService paymentService)
|
||||
{
|
||||
Organization organization = null;
|
||||
if (model.OrganizationId != default)
|
||||
{
|
||||
if (!await currentContext.EditPaymentMethods(model.OrganizationId))
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
organization = await organizationRepository.GetByIdAsync(model.OrganizationId);
|
||||
if (organization == null)
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
var invoice = await paymentService.PreviewInvoiceAsync(model, organization?.GatewayCustomerId,
|
||||
organization?.GatewaySubscriptionId);
|
||||
|
||||
return TypedResults.Ok(invoice);
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
|
||||
using Bit.Core.Billing.Models.Business;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
using Bit.Core.Billing.Organizations.Queries;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Api.OrganizationLicenses;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Billing.Controllers;
|
||||
|
||||
[Route("licenses")]
|
||||
[Authorize("Licensing")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public class LicensesController : Controller
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IGetCloudOrganizationLicenseQuery _getCloudOrganizationLicenseQuery;
|
||||
private readonly IValidateBillingSyncKeyCommand _validateBillingSyncKeyCommand;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
|
||||
public LicensesController(
|
||||
IUserRepository userRepository,
|
||||
IUserService userService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IGetCloudOrganizationLicenseQuery getCloudOrganizationLicenseQuery,
|
||||
IValidateBillingSyncKeyCommand validateBillingSyncKeyCommand,
|
||||
ICurrentContext currentContext)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_userService = userService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_getCloudOrganizationLicenseQuery = getCloudOrganizationLicenseQuery;
|
||||
_validateBillingSyncKeyCommand = validateBillingSyncKeyCommand;
|
||||
_currentContext = currentContext;
|
||||
}
|
||||
|
||||
[HttpGet("user/{id}")]
|
||||
public async Task<UserLicense> GetUser(string id, [FromQuery] string key)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(new Guid(id));
|
||||
if (user == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else if (!user.LicenseKey.Equals(key))
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
throw new BadRequestException("Invalid license key.");
|
||||
}
|
||||
|
||||
var license = await _userService.GenerateLicenseAsync(user, null);
|
||||
return license;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used by self-hosted installations to get an updated license file
|
||||
/// </summary>
|
||||
[HttpGet("organization/{id}")]
|
||||
public async Task<OrganizationLicense> OrganizationSync(string id, [FromBody] SelfHostedOrganizationLicenseRequestModel model)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(new Guid(id));
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException("Organization not found.");
|
||||
}
|
||||
|
||||
if (!organization.LicenseKey.Equals(model.LicenseKey))
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
throw new BadRequestException("Invalid license key.");
|
||||
}
|
||||
|
||||
if (!await _validateBillingSyncKeyCommand.ValidateBillingSyncKeyAsync(organization, model.BillingSyncKey))
|
||||
{
|
||||
throw new BadRequestException("Invalid Billing Sync Key");
|
||||
}
|
||||
|
||||
var license = await _getCloudOrganizationLicenseQuery.GetLicenseAsync(organization, _currentContext.InstallationId.Value);
|
||||
return license;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -19,10 +18,10 @@ public class OrganizationBillingController(
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationBillingService organizationBillingService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPaymentService paymentService,
|
||||
ISubscriberService subscriberService,
|
||||
IStripePaymentService paymentService,
|
||||
IPaymentHistoryService paymentHistoryService) : BaseBillingController
|
||||
{
|
||||
// TODO: Remove when pm-25379-use-new-organization-metadata-structure is removed.
|
||||
[HttpGet("metadata")]
|
||||
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
|
||||
{
|
||||
@@ -41,6 +40,7 @@ public class OrganizationBillingController(
|
||||
return TypedResults.Ok(metadata);
|
||||
}
|
||||
|
||||
// TODO: Migrate to Query / OrganizationBillingVNextController
|
||||
[HttpGet("history")]
|
||||
public async Task<IResult> GetHistoryAsync([FromRoute] Guid organizationId)
|
||||
{
|
||||
@@ -61,6 +61,7 @@ public class OrganizationBillingController(
|
||||
return TypedResults.Ok(billingInfo);
|
||||
}
|
||||
|
||||
// TODO: Migrate to Query / OrganizationBillingVNextController
|
||||
[HttpGet("invoices")]
|
||||
public async Task<IResult> GetInvoicesAsync([FromRoute] Guid organizationId, [FromQuery] string? status = null, [FromQuery] string? startAfter = null)
|
||||
{
|
||||
@@ -85,6 +86,7 @@ public class OrganizationBillingController(
|
||||
return TypedResults.Ok(invoices);
|
||||
}
|
||||
|
||||
// TODO: Migrate to Query / OrganizationBillingVNextController
|
||||
[HttpGet("transactions")]
|
||||
public async Task<IResult> GetTransactionsAsync([FromRoute] Guid organizationId, [FromQuery] DateTime? startAfter = null)
|
||||
{
|
||||
@@ -108,6 +110,7 @@ public class OrganizationBillingController(
|
||||
return TypedResults.Ok(transactions);
|
||||
}
|
||||
|
||||
// TODO: Can be removed once we do away with the organization-plans.component.
|
||||
[HttpGet]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IResult> GetBillingAsync(Guid organizationId)
|
||||
@@ -131,127 +134,7 @@ public class OrganizationBillingController(
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("payment-method")]
|
||||
public async Task<IResult> GetPaymentMethodAsync([FromRoute] Guid organizationId)
|
||||
{
|
||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
var paymentMethod = await subscriberService.GetPaymentMethod(organization);
|
||||
|
||||
var response = PaymentMethodResponse.From(paymentMethod);
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
[HttpPut("payment-method")]
|
||||
public async Task<IResult> UpdatePaymentMethodAsync(
|
||||
[FromRoute] Guid organizationId,
|
||||
[FromBody] UpdatePaymentMethodRequestBody requestBody)
|
||||
{
|
||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
var tokenizedPaymentSource = requestBody.PaymentSource.ToDomain();
|
||||
|
||||
var taxInformation = requestBody.TaxInformation.ToDomain();
|
||||
|
||||
await organizationBillingService.UpdatePaymentMethod(organization, tokenizedPaymentSource, taxInformation);
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
[HttpPost("payment-method/verify-bank-account")]
|
||||
public async Task<IResult> VerifyBankAccountAsync(
|
||||
[FromRoute] Guid organizationId,
|
||||
[FromBody] VerifyBankAccountRequestBody requestBody)
|
||||
{
|
||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
if (requestBody.DescriptorCode.Length != 6 || !requestBody.DescriptorCode.StartsWith("SM"))
|
||||
{
|
||||
return Error.BadRequest("Statement descriptor should be a 6-character value that starts with 'SM'");
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
await subscriberService.VerifyBankAccount(organization, requestBody.DescriptorCode);
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
[HttpGet("tax-information")]
|
||||
public async Task<IResult> GetTaxInformationAsync([FromRoute] Guid organizationId)
|
||||
{
|
||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
var taxInformation = await subscriberService.GetTaxInformation(organization);
|
||||
|
||||
var response = TaxInformationResponse.From(taxInformation);
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
[HttpPut("tax-information")]
|
||||
public async Task<IResult> UpdateTaxInformationAsync(
|
||||
[FromRoute] Guid organizationId,
|
||||
[FromBody] TaxInformationRequestBody requestBody)
|
||||
{
|
||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
var taxInformation = requestBody.ToDomain();
|
||||
|
||||
await subscriberService.UpdateTaxInformation(organization, taxInformation);
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
// TODO: Migrate to Command / OrganizationBillingVNextController
|
||||
[HttpPost("setup-business-unit")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IResult> SetupBusinessUnitAsync(
|
||||
@@ -280,6 +163,7 @@ public class OrganizationBillingController(
|
||||
return TypedResults.Ok(providerId);
|
||||
}
|
||||
|
||||
// TODO: Migrate to Command / OrganizationBillingVNextController
|
||||
[HttpPost("change-frequency")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IResult> ChangePlanSubscriptionFrequencyAsync(
|
||||
|
||||
@@ -19,7 +19,6 @@ using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@@ -37,7 +36,7 @@ public class OrganizationsController(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationService organizationService,
|
||||
IUserService userService,
|
||||
IPaymentService paymentService,
|
||||
IStripePaymentService paymentService,
|
||||
ICurrentContext currentContext,
|
||||
IGetCloudOrganizationLicenseQuery getCloudOrganizationLicenseQuery,
|
||||
GlobalSettings globalSettings,
|
||||
@@ -67,7 +66,8 @@ public class OrganizationsController(
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
var orgLicense = await licensingService.ReadOrganizationLicenseAsync(organization);
|
||||
return new OrganizationSubscriptionResponseModel(organization, orgLicense);
|
||||
var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(orgLicense);
|
||||
return new OrganizationSubscriptionResponseModel(organization, orgLicense, claimsPrincipal);
|
||||
}
|
||||
|
||||
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
@@ -248,53 +248,6 @@ public class OrganizationsController(
|
||||
await organizationService.ReinstateSubscriptionAsync(id);
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/tax")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<TaxInfoResponseModel> GetTaxInfo(Guid id)
|
||||
{
|
||||
if (!await currentContext.OrganizationOwner(id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(id);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var taxInfo = await paymentService.GetTaxInfoAsync(organization);
|
||||
return new TaxInfoResponseModel(taxInfo);
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/tax")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PutTaxInfo(Guid id, [FromBody] ExpandedTaxInfoUpdateRequestModel model)
|
||||
{
|
||||
if (!await currentContext.OrganizationOwner(id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(id);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var taxInfo = new TaxInfo
|
||||
{
|
||||
TaxIdNumber = model.TaxId,
|
||||
BillingAddressLine1 = model.Line1,
|
||||
BillingAddressLine2 = model.Line2,
|
||||
BillingAddressCity = model.City,
|
||||
BillingAddressState = model.State,
|
||||
BillingAddressPostalCode = model.PostalCode,
|
||||
BillingAddressCountry = model.Country,
|
||||
};
|
||||
await paymentService.SaveTaxInfoAsync(organization, taxInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to grant owner access to the Secrets Manager for the organization
|
||||
/// </summary>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
@@ -9,7 +8,6 @@ using Bit.Core.Billing.Providers.Models;
|
||||
using Bit.Core.Billing.Providers.Repositories;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Models.BitStripe;
|
||||
using Bit.Core.Services;
|
||||
@@ -34,6 +32,7 @@ public class ProviderBillingController(
|
||||
IStripeAdapter stripeAdapter,
|
||||
IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService)
|
||||
{
|
||||
// TODO: Migrate to Query / ProviderBillingVNextController
|
||||
[HttpGet("invoices")]
|
||||
public async Task<IResult> GetInvoicesAsync([FromRoute] Guid providerId)
|
||||
{
|
||||
@@ -44,7 +43,7 @@ public class ProviderBillingController(
|
||||
return result;
|
||||
}
|
||||
|
||||
var invoices = await stripeAdapter.InvoiceListAsync(new StripeInvoiceListOptions
|
||||
var invoices = await stripeAdapter.ListInvoicesAsync(new StripeInvoiceListOptions
|
||||
{
|
||||
Customer = provider.GatewayCustomerId
|
||||
});
|
||||
@@ -54,6 +53,7 @@ public class ProviderBillingController(
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
// TODO: Migrate to Query / ProviderBillingVNextController
|
||||
[HttpGet("invoices/{invoiceId}")]
|
||||
public async Task<IResult> GenerateClientInvoiceReportAsync([FromRoute] Guid providerId, string invoiceId)
|
||||
{
|
||||
@@ -76,51 +76,7 @@ public class ProviderBillingController(
|
||||
"text/csv");
|
||||
}
|
||||
|
||||
[HttpPut("payment-method")]
|
||||
public async Task<IResult> UpdatePaymentMethodAsync(
|
||||
[FromRoute] Guid providerId,
|
||||
[FromBody] UpdatePaymentMethodRequestBody requestBody)
|
||||
{
|
||||
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
|
||||
|
||||
if (provider == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var tokenizedPaymentSource = requestBody.PaymentSource.ToDomain();
|
||||
var taxInformation = requestBody.TaxInformation.ToDomain();
|
||||
|
||||
await providerBillingService.UpdatePaymentMethod(
|
||||
provider,
|
||||
tokenizedPaymentSource,
|
||||
taxInformation);
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
[HttpPost("payment-method/verify-bank-account")]
|
||||
public async Task<IResult> VerifyBankAccountAsync(
|
||||
[FromRoute] Guid providerId,
|
||||
[FromBody] VerifyBankAccountRequestBody requestBody)
|
||||
{
|
||||
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
|
||||
|
||||
if (provider == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (requestBody.DescriptorCode.Length != 6 || !requestBody.DescriptorCode.StartsWith("SM"))
|
||||
{
|
||||
return Error.BadRequest("Statement descriptor should be a 6-character value that starts with 'SM'");
|
||||
}
|
||||
|
||||
await subscriberService.VerifyBankAccount(provider, requestBody.DescriptorCode);
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
// TODO: Migrate to Query / ProviderBillingVNextController
|
||||
[HttpGet("subscription")]
|
||||
public async Task<IResult> GetSubscriptionAsync([FromRoute] Guid providerId)
|
||||
{
|
||||
@@ -131,7 +87,7 @@ public class ProviderBillingController(
|
||||
return result;
|
||||
}
|
||||
|
||||
var subscription = await stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId,
|
||||
var subscription = await stripeAdapter.GetSubscriptionAsync(provider.GatewaySubscriptionId,
|
||||
new SubscriptionGetOptions { Expand = ["customer.tax_ids", "discounts", "test_clock"] });
|
||||
|
||||
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
||||
@@ -140,7 +96,7 @@ public class ProviderBillingController(
|
||||
{
|
||||
var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
|
||||
var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, plan.Type);
|
||||
var price = await stripeAdapter.PriceGetAsync(priceId);
|
||||
var price = await stripeAdapter.GetPriceAsync(priceId);
|
||||
|
||||
var unitAmount = price.UnitAmountDecimal.HasValue
|
||||
? price.UnitAmountDecimal.Value / 100M
|
||||
@@ -172,53 +128,4 @@ public class ProviderBillingController(
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("tax-information")]
|
||||
public async Task<IResult> GetTaxInformationAsync([FromRoute] Guid providerId)
|
||||
{
|
||||
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
|
||||
|
||||
if (provider == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var taxInformation = await subscriberService.GetTaxInformation(provider);
|
||||
|
||||
var response = TaxInformationResponse.From(taxInformation);
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
[HttpPut("tax-information")]
|
||||
public async Task<IResult> UpdateTaxInformationAsync(
|
||||
[FromRoute] Guid providerId,
|
||||
[FromBody] TaxInformationRequestBody requestBody)
|
||||
{
|
||||
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
|
||||
|
||||
if (provider == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (requestBody is not { Country: not null, PostalCode: not null })
|
||||
{
|
||||
return Error.BadRequest("Country and postal code are required to update your tax information.");
|
||||
}
|
||||
|
||||
var taxInformation = new TaxInformation(
|
||||
requestBody.Country,
|
||||
requestBody.PostalCode,
|
||||
requestBody.TaxId,
|
||||
requestBody.TaxIdType,
|
||||
requestBody.Line1,
|
||||
requestBody.Line2,
|
||||
requestBody.City,
|
||||
requestBody.State);
|
||||
|
||||
await subscriberService.UpdateTaxInformation(provider, taxInformation);
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -28,7 +28,7 @@ public class StripeController(
|
||||
Usage = "off_session"
|
||||
};
|
||||
|
||||
var setupIntent = await stripeAdapter.SetupIntentCreate(options);
|
||||
var setupIntent = await stripeAdapter.CreateSetupIntentAsync(options);
|
||||
|
||||
return TypedResults.Ok(setupIntent.ClientSecret);
|
||||
}
|
||||
@@ -43,7 +43,7 @@ public class StripeController(
|
||||
Usage = "off_session"
|
||||
};
|
||||
|
||||
var setupIntent = await stripeAdapter.SetupIntentCreate(options);
|
||||
var setupIntent = await stripeAdapter.CreateSetupIntentAsync(options);
|
||||
|
||||
return TypedResults.Ok(setupIntent.ClientSecret);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable enable
|
||||
using Bit.Api.Billing.Attributes;
|
||||
using Bit.Api.Billing.Attributes;
|
||||
using Bit.Api.Billing.Models.Requests.Premium;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Core;
|
||||
@@ -17,7 +16,7 @@ namespace Bit.Api.Billing.Controllers.VNext;
|
||||
[Authorize("Application")]
|
||||
[Route("account/billing/vnext/self-host")]
|
||||
[SelfHosted(SelfHostedOnly = true)]
|
||||
public class SelfHostedAccountBillingController(
|
||||
public class SelfHostedAccountBillingVNextController(
|
||||
ICreatePremiumSelfHostedSubscriptionCommand createPremiumSelfHostedSubscriptionCommand) : BaseBillingController
|
||||
{
|
||||
[HttpPost("license")]
|
||||
@@ -14,7 +14,7 @@ namespace Bit.Api.Billing.Controllers.VNext;
|
||||
[Authorize("Application")]
|
||||
[Route("organizations/{organizationId:guid}/billing/vnext/self-host")]
|
||||
[SelfHosted(SelfHostedOnly = true)]
|
||||
public class SelfHostedBillingController(
|
||||
public class SelfHostedOrganizationBillingVNextController(
|
||||
IGetOrganizationMetadataQuery getOrganizationMetadataQuery) : BaseBillingController
|
||||
{
|
||||
[Authorize<MemberOrProviderRequirement>]
|
||||
@@ -1,31 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests;
|
||||
|
||||
public class TaxInformationRequestBody
|
||||
{
|
||||
[Required]
|
||||
public string Country { get; set; }
|
||||
[Required]
|
||||
public string PostalCode { get; set; }
|
||||
public string TaxId { get; set; }
|
||||
public string TaxIdType { get; set; }
|
||||
public string Line1 { get; set; }
|
||||
public string Line2 { get; set; }
|
||||
public string City { get; set; }
|
||||
public string State { get; set; }
|
||||
|
||||
public TaxInformation ToDomain() => new(
|
||||
Country,
|
||||
PostalCode,
|
||||
TaxId,
|
||||
TaxIdType,
|
||||
Line1,
|
||||
Line2,
|
||||
City,
|
||||
State);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests;
|
||||
|
||||
public class TokenizedPaymentSourceRequestBody
|
||||
{
|
||||
[Required]
|
||||
[EnumMatches<PaymentMethodType>(
|
||||
PaymentMethodType.BankAccount,
|
||||
PaymentMethodType.Card,
|
||||
PaymentMethodType.PayPal,
|
||||
ErrorMessage = "'type' must be BankAccount, Card or PayPal")]
|
||||
public PaymentMethodType Type { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Token { get; set; }
|
||||
|
||||
public TokenizedPaymentSource ToDomain() => new(Type, Token);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests;
|
||||
|
||||
public class UpdatePaymentMethodRequestBody
|
||||
{
|
||||
[Required]
|
||||
public TokenizedPaymentSourceRequestBody PaymentSource { get; set; }
|
||||
|
||||
[Required]
|
||||
public TaxInformationRequestBody TaxInformation { get; set; }
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests;
|
||||
|
||||
public class VerifyBankAccountRequestBody
|
||||
{
|
||||
[Required]
|
||||
public string DescriptorCode { get; set; }
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public class BillingPaymentResponseModel : ResponseModel
|
||||
{
|
||||
public BillingPaymentResponseModel(BillingInfo billing)
|
||||
: base("billingPayment")
|
||||
{
|
||||
Balance = billing.Balance;
|
||||
PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null;
|
||||
}
|
||||
|
||||
public decimal Balance { get; set; }
|
||||
public BillingSource PaymentSource { get; set; }
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public record PaymentMethodResponse(
|
||||
decimal AccountCredit,
|
||||
PaymentSource PaymentSource,
|
||||
string SubscriptionStatus,
|
||||
TaxInformation TaxInformation)
|
||||
{
|
||||
public static PaymentMethodResponse From(PaymentMethod paymentMethod) =>
|
||||
new(
|
||||
paymentMethod.AccountCredit,
|
||||
paymentMethod.PaymentSource,
|
||||
paymentMethod.SubscriptionStatus,
|
||||
paymentMethod.TaxInformation);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public record PaymentSourceResponse(
|
||||
PaymentMethodType Type,
|
||||
string Description,
|
||||
bool NeedsVerification)
|
||||
{
|
||||
public static PaymentSourceResponse From(PaymentSource paymentMethod)
|
||||
=> new(
|
||||
paymentMethod.Type,
|
||||
paymentMethod.Description,
|
||||
paymentMethod.NeedsVerification);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public record TaxInformationResponse(
|
||||
string Country,
|
||||
string PostalCode,
|
||||
string TaxId,
|
||||
string Line1,
|
||||
string Line2,
|
||||
string City,
|
||||
string State)
|
||||
{
|
||||
public static TaxInformationResponse From(TaxInformation taxInformation)
|
||||
=> new(
|
||||
taxInformation.Country,
|
||||
taxInformation.PostalCode,
|
||||
taxInformation.TaxId,
|
||||
taxInformation.Line1,
|
||||
taxInformation.Line2,
|
||||
taxInformation.City,
|
||||
taxInformation.State);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,10 @@ public class HibpController : Controller
|
||||
}
|
||||
else if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return new NotFoundResult();
|
||||
/* 12/1/2025 - Per the HIBP API, If the domain does not have any email addresses in any breaches,
|
||||
an HTTP 404 response will be returned. API also specifies that "404 Not found is the account could
|
||||
not be found and has therefore not been pwned". Per REST semantics we will return 200 OK with empty array. */
|
||||
return Content("[]", "application/json");
|
||||
}
|
||||
else if (response.StatusCode == HttpStatusCode.TooManyRequests && retry)
|
||||
{
|
||||
|
||||
@@ -59,13 +59,6 @@ public class JobsHostedService : BaseJobsHostedService
|
||||
.StartNow()
|
||||
.WithCronSchedule("0 0 * * * ?")
|
||||
.Build();
|
||||
var updatePhishingDomainsTrigger = TriggerBuilder.Create()
|
||||
.WithIdentity("UpdatePhishingDomainsTrigger")
|
||||
.StartNow()
|
||||
.WithSimpleSchedule(x => x
|
||||
.WithIntervalInHours(24)
|
||||
.RepeatForever())
|
||||
.Build();
|
||||
var updateOrgSubscriptionsTrigger = TriggerBuilder.Create()
|
||||
.WithIdentity("UpdateOrgSubscriptionsTrigger")
|
||||
.StartNow()
|
||||
@@ -81,7 +74,6 @@ 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),
|
||||
new (typeof(OrganizationSubscriptionUpdateJob), updateOrgSubscriptionsTrigger),
|
||||
};
|
||||
|
||||
@@ -111,7 +103,6 @@ public class JobsHostedService : BaseJobsHostedService
|
||||
services.AddTransient<ValidateUsersJob>();
|
||||
services.AddTransient<ValidateOrganizationsJob>();
|
||||
services.AddTransient<ValidateOrganizationDomainJob>();
|
||||
services.AddTransient<UpdatePhishingDomainsJob>();
|
||||
services.AddTransient<OrganizationSubscriptionUpdateJob>();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
#nullable enable
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Auth.Models.Request;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Api.KeyManagement.Models.Requests;
|
||||
using Bit.Api.KeyManagement.Models.Responses;
|
||||
using Bit.Api.KeyManagement.Validators;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
@@ -14,6 +14,7 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Commands.Interfaces;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@@ -45,11 +46,13 @@ public class AccountsKeyManagementController : Controller
|
||||
private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
|
||||
_webauthnKeyValidator;
|
||||
private readonly IRotationValidator<IEnumerable<OtherDeviceKeysUpdateRequestModel>, IEnumerable<Device>> _deviceValidator;
|
||||
private readonly IKeyConnectorConfirmationDetailsQuery _keyConnectorConfirmationDetailsQuery;
|
||||
|
||||
public AccountsKeyManagementController(IUserService userService,
|
||||
IFeatureService featureService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IEmergencyAccessRepository emergencyAccessRepository,
|
||||
IKeyConnectorConfirmationDetailsQuery keyConnectorConfirmationDetailsQuery,
|
||||
IRegenerateUserAsymmetricKeysCommand regenerateUserAsymmetricKeysCommand,
|
||||
IRotateUserAccountKeysCommand rotateUserKeyCommandV2,
|
||||
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
|
||||
@@ -75,12 +78,13 @@ public class AccountsKeyManagementController : Controller
|
||||
_organizationUserValidator = organizationUserValidator;
|
||||
_webauthnKeyValidator = webAuthnKeyValidator;
|
||||
_deviceValidator = deviceValidator;
|
||||
_keyConnectorConfirmationDetailsQuery = keyConnectorConfirmationDetailsQuery;
|
||||
}
|
||||
|
||||
[HttpPost("key-management/regenerate-keys")]
|
||||
public async Task RegenerateKeysAsync([FromBody] KeyRegenerationRequestModel request)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration))
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration) && !_featureService.IsEnabled(FeatureFlagKeys.DataRecoveryTool))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@@ -178,4 +182,17 @@ public class AccountsKeyManagementController : Controller
|
||||
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
[HttpGet("key-connector/confirmation-details/{orgSsoIdentifier}")]
|
||||
public async Task<KeyConnectorConfirmationDetailsResponseModel> GetKeyConnectorConfirmationDetailsAsync(string orgSsoIdentifier)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var details = await _keyConnectorConfirmationDetailsQuery.Run(orgSsoIdentifier, user.Id);
|
||||
return new KeyConnectorConfirmationDetailsResponseModel(details);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Responses;
|
||||
|
||||
public class KeyConnectorConfirmationDetailsResponseModel : ResponseModel
|
||||
{
|
||||
private const string _objectName = "keyConnectorConfirmationDetails";
|
||||
|
||||
public KeyConnectorConfirmationDetailsResponseModel(KeyConnectorConfirmationDetails details,
|
||||
string obj = _objectName) : base(obj)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(details);
|
||||
|
||||
OrganizationName = details.OrganizationName;
|
||||
}
|
||||
|
||||
public KeyConnectorConfirmationDetailsResponseModel() : base(_objectName)
|
||||
{
|
||||
OrganizationName = string.Empty;
|
||||
}
|
||||
|
||||
public string OrganizationName { get; set; }
|
||||
}
|
||||
@@ -1,27 +1,32 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Api.Response;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.Models.Response;
|
||||
|
||||
public class KeysResponseModel : ResponseModel
|
||||
{
|
||||
public KeysResponseModel(User user)
|
||||
public KeysResponseModel(UserAccountKeysData accountKeys, string? masterKeyWrappedUserKey)
|
||||
: base("keys")
|
||||
{
|
||||
if (user == null)
|
||||
if (masterKeyWrappedUserKey != null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
Key = masterKeyWrappedUserKey;
|
||||
}
|
||||
|
||||
Key = user.Key;
|
||||
PublicKey = user.PublicKey;
|
||||
PrivateKey = user.PrivateKey;
|
||||
PublicKey = accountKeys.PublicKeyEncryptionKeyPairData.PublicKey;
|
||||
PrivateKey = accountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey;
|
||||
AccountKeys = new PrivateKeysResponseModel(accountKeys);
|
||||
}
|
||||
|
||||
public string Key { get; set; }
|
||||
/// <summary>
|
||||
/// The master key wrapped user key. The master key can either be a master-password master key or a
|
||||
/// key-connector master key.
|
||||
/// </summary>
|
||||
public string? Key { get; set; }
|
||||
[Obsolete("Use AccountKeys.PublicKeyEncryptionKeyPair.PublicKey instead")]
|
||||
public string PublicKey { get; set; }
|
||||
[Obsolete("Use AccountKeys.PublicKeyEncryptionKeyPair.WrappedPrivateKey instead")]
|
||||
public string PrivateKey { get; set; }
|
||||
public PrivateKeysResponseModel AccountKeys { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using Bit.Core.Billing.Constants;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Licenses;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Billing.Models.Business;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Api;
|
||||
@@ -37,6 +40,46 @@ public class SubscriptionResponseModel : ResponseModel
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <param name="user">The user entity containing storage and premium subscription information</param>
|
||||
/// <param name="subscription">Subscription information retrieved from the payment provider (Stripe/Braintree)</param>
|
||||
/// <param name="license">The user's license containing expiration and feature entitlements</param>
|
||||
/// <param name="claimsPrincipal">The claims principal containing cryptographically secure token claims</param>
|
||||
/// <param name="includeMilestone2Discount">
|
||||
/// Whether to include discount information in the response.
|
||||
/// Set to true when the PM23341_Milestone_2 feature flag is enabled AND
|
||||
/// you want to expose Milestone 2 discount information to the client.
|
||||
/// The discount will only be included if it matches the specific Milestone 2 coupon ID.
|
||||
/// </param>
|
||||
public SubscriptionResponseModel(User user, SubscriptionInfo? subscription, UserLicense license, ClaimsPrincipal? claimsPrincipal, bool includeMilestone2Discount = false)
|
||||
: base("subscription")
|
||||
{
|
||||
Subscription = subscription?.Subscription != null ? new BillingSubscription(subscription.Subscription) : null;
|
||||
UpcomingInvoice = subscription?.UpcomingInvoice != null ?
|
||||
new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null;
|
||||
StorageName = user.Storage.HasValue ? CoreHelpers.ReadableBytesSize(user.Storage.Value) : null;
|
||||
StorageGb = user.Storage.HasValue ? Math.Round(user.Storage.Value / 1073741824D, 2) : 0; // 1 GB
|
||||
MaxStorageGb = user.MaxStorageGb;
|
||||
License = license;
|
||||
|
||||
// CRITICAL: When a license has a Token (JWT), ALWAYS use the expiration from the token claim
|
||||
// The token's expiration is cryptographically secured and cannot be tampered with
|
||||
// The file's Expires property can be manually edited and should NOT be trusted for display
|
||||
if (claimsPrincipal != null)
|
||||
{
|
||||
Expiration = claimsPrincipal.GetValue<DateTime?>(UserLicenseConstants.Expires);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No token - use the license file expiration (for older licenses without tokens)
|
||||
Expiration = License.Expires;
|
||||
}
|
||||
|
||||
// Only display the Milestone 2 subscription discount on the subscription page.
|
||||
CustomerDiscount = ShouldIncludeMilestone2Discount(includeMilestone2Discount, subscription?.CustomerDiscount)
|
||||
? new BillingCustomerDiscount(subscription!.CustomerDiscount!)
|
||||
: null;
|
||||
}
|
||||
|
||||
public SubscriptionResponseModel(User user, UserLicense? license = null)
|
||||
: base("subscription")
|
||||
{
|
||||
|
||||
337
src/Api/SecretsManager/Controllers/SecretVersionsController.cs
Normal file
337
src/Api/SecretsManager/Controllers/SecretVersionsController.cs
Normal file
@@ -0,0 +1,337 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.SecretsManager.Models.Request;
|
||||
using Bit.Api.SecretsManager.Models.Response;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.SecretsManager.Controllers;
|
||||
|
||||
[Authorize("secrets")]
|
||||
public class SecretVersionsController : Controller
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ISecretVersionRepository _secretVersionRepository;
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
|
||||
public SecretVersionsController(
|
||||
ICurrentContext currentContext,
|
||||
ISecretVersionRepository secretVersionRepository,
|
||||
ISecretRepository secretRepository,
|
||||
IUserService userService,
|
||||
IOrganizationUserRepository organizationUserRepository)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_secretVersionRepository = secretVersionRepository;
|
||||
_secretRepository = secretRepository;
|
||||
_userService = userService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
}
|
||||
|
||||
[HttpGet("secrets/{secretId}/versions")]
|
||||
public async Task<ListResponseModel<SecretVersionResponseModel>> GetVersionsBySecretIdAsync([FromRoute] Guid secretId)
|
||||
{
|
||||
var secret = await _secretRepository.GetByIdAsync(secretId);
|
||||
if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// For service accounts and organization API, skip user-level access checks
|
||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount ||
|
||||
_currentContext.IdentityClientType == IdentityClientType.Organization)
|
||||
{
|
||||
// Already verified Secrets Manager access above
|
||||
var versionList = await _secretVersionRepository.GetManyBySecretIdAsync(secretId);
|
||||
var responseList = versionList.Select(v => new SecretVersionResponseModel(v));
|
||||
return new ListResponseModel<SecretVersionResponseModel>(responseList);
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
|
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);
|
||||
|
||||
var access = await _secretRepository.AccessToSecretAsync(secretId, userId.Value, accessClient);
|
||||
if (!access.Read)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var versions = await _secretVersionRepository.GetManyBySecretIdAsync(secretId);
|
||||
var responses = versions.Select(v => new SecretVersionResponseModel(v));
|
||||
|
||||
return new ListResponseModel<SecretVersionResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpGet("secret-versions/{id}")]
|
||||
public async Task<SecretVersionResponseModel> GetByIdAsync([FromRoute] Guid id)
|
||||
{
|
||||
var secretVersion = await _secretVersionRepository.GetByIdAsync(id);
|
||||
if (secretVersion == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var secret = await _secretRepository.GetByIdAsync(secretVersion.SecretId);
|
||||
if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// For service accounts and organization API, skip user-level access checks
|
||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount ||
|
||||
_currentContext.IdentityClientType == IdentityClientType.Organization)
|
||||
{
|
||||
// Already verified Secrets Manager access above
|
||||
return new SecretVersionResponseModel(secretVersion);
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
|
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);
|
||||
|
||||
var access = await _secretRepository.AccessToSecretAsync(secretVersion.SecretId, userId.Value, accessClient);
|
||||
if (!access.Read)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new SecretVersionResponseModel(secretVersion);
|
||||
}
|
||||
|
||||
[HttpPost("secret-versions/get-by-ids")]
|
||||
public async Task<ListResponseModel<SecretVersionResponseModel>> GetManyByIdsAsync([FromBody] List<Guid> ids)
|
||||
{
|
||||
if (!ids.Any())
|
||||
{
|
||||
throw new BadRequestException("No version IDs provided.");
|
||||
}
|
||||
|
||||
// Get all versions
|
||||
var versions = (await _secretVersionRepository.GetManyByIdsAsync(ids)).ToList();
|
||||
if (!versions.Any())
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Get all associated secrets and check permissions
|
||||
var secretIds = versions.Select(v => v.SecretId).Distinct().ToList();
|
||||
var secrets = (await _secretRepository.GetManyByIds(secretIds)).ToList();
|
||||
|
||||
if (!secrets.Any())
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Ensure all secrets belong to the same organization
|
||||
var organizationId = secrets.First().OrganizationId;
|
||||
if (secrets.Any(s => s.OrganizationId != organizationId) ||
|
||||
!_currentContext.AccessSecretsManager(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// For service accounts and organization API, skip user-level access checks
|
||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount ||
|
||||
_currentContext.IdentityClientType == IdentityClientType.Organization)
|
||||
{
|
||||
// Already verified Secrets Manager access and organization ownership above
|
||||
var serviceAccountResponses = versions.Select(v => new SecretVersionResponseModel(v));
|
||||
return new ListResponseModel<SecretVersionResponseModel>(serviceAccountResponses);
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var isAdmin = await _currentContext.OrganizationAdmin(organizationId);
|
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, isAdmin);
|
||||
|
||||
// Verify read access to all associated secrets
|
||||
var accessResults = await _secretRepository.AccessToSecretsAsync(secretIds, userId.Value, accessClient);
|
||||
if (accessResults.Values.Any(access => !access.Read))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var responses = versions.Select(v => new SecretVersionResponseModel(v));
|
||||
return new ListResponseModel<SecretVersionResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpPut("secrets/{secretId}/versions/restore")]
|
||||
public async Task<SecretResponseModel> RestoreVersionAsync([FromRoute] Guid secretId, [FromBody] RestoreSecretVersionRequestModel request)
|
||||
{
|
||||
if (!(_currentContext.IdentityClientType == IdentityClientType.User || _currentContext.IdentityClientType == IdentityClientType.ServiceAccount))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var secret = await _secretRepository.GetByIdAsync(secretId);
|
||||
if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Get the version first to validate it belongs to this secret
|
||||
var version = await _secretVersionRepository.GetByIdAsync(request.VersionId);
|
||||
if (version == null || version.SecretId != secretId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Store the current value before restoration
|
||||
var currentValue = secret.Value;
|
||||
|
||||
// For service accounts and organization API, skip user-level access checks
|
||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount)
|
||||
{
|
||||
// Save current value as a version before restoring
|
||||
if (currentValue != version.Value)
|
||||
{
|
||||
var editorUserId = _userService.GetProperUserId(User);
|
||||
if (editorUserId.HasValue)
|
||||
{
|
||||
var currentVersionSnapshot = new Core.SecretsManager.Entities.SecretVersion
|
||||
{
|
||||
SecretId = secretId,
|
||||
Value = currentValue!,
|
||||
VersionDate = DateTime.UtcNow,
|
||||
EditorServiceAccountId = editorUserId.Value
|
||||
};
|
||||
|
||||
await _secretVersionRepository.CreateAsync(currentVersionSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
// Already verified Secrets Manager access above
|
||||
secret.Value = version.Value;
|
||||
secret.RevisionDate = DateTime.UtcNow;
|
||||
var updatedSec = await _secretRepository.UpdateAsync(secret);
|
||||
return new SecretResponseModel(updatedSec, true, true);
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
|
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);
|
||||
|
||||
var access = await _secretRepository.AccessToSecretAsync(secretId, userId.Value, accessClient);
|
||||
if (!access.Write)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Save current value as a version before restoring
|
||||
if (currentValue != version.Value)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(secret.OrganizationId, userId.Value);
|
||||
if (orgUser == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var currentVersionSnapshot = new Core.SecretsManager.Entities.SecretVersion
|
||||
{
|
||||
SecretId = secretId,
|
||||
Value = currentValue!,
|
||||
VersionDate = DateTime.UtcNow,
|
||||
EditorOrganizationUserId = orgUser.Id
|
||||
};
|
||||
|
||||
await _secretVersionRepository.CreateAsync(currentVersionSnapshot);
|
||||
}
|
||||
|
||||
// Update the secret with the version's value
|
||||
secret.Value = version.Value;
|
||||
secret.RevisionDate = DateTime.UtcNow;
|
||||
|
||||
var updatedSecret = await _secretRepository.UpdateAsync(secret);
|
||||
|
||||
return new SecretResponseModel(updatedSecret, true, true);
|
||||
}
|
||||
|
||||
[HttpPost("secret-versions/delete")]
|
||||
public async Task<IActionResult> BulkDeleteAsync([FromBody] List<Guid> ids)
|
||||
{
|
||||
if (!ids.Any())
|
||||
{
|
||||
throw new BadRequestException("No version IDs provided.");
|
||||
}
|
||||
|
||||
var secretVersions = (await _secretVersionRepository.GetManyByIdsAsync(ids)).ToList();
|
||||
if (secretVersions.Count != ids.Count)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Ensure all versions belong to secrets in the same organization
|
||||
var secretIds = secretVersions.Select(v => v.SecretId).Distinct().ToList();
|
||||
var secrets = await _secretRepository.GetManyByIds(secretIds);
|
||||
var secretsList = secrets.ToList();
|
||||
|
||||
if (!secretsList.Any())
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organizationId = secretsList.First().OrganizationId;
|
||||
if (secretsList.Any(s => s.OrganizationId != organizationId) ||
|
||||
!_currentContext.AccessSecretsManager(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// For service accounts and organization API, skip user-level access checks
|
||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount ||
|
||||
_currentContext.IdentityClientType == IdentityClientType.Organization)
|
||||
{
|
||||
// Already verified Secrets Manager access and organization ownership above
|
||||
await _secretVersionRepository.DeleteManyByIdAsync(ids);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
|
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);
|
||||
|
||||
// Verify write access to all associated secrets
|
||||
var accessResults = await _secretRepository.AccessToSecretsAsync(secretIds, userId.Value, accessClient);
|
||||
if (accessResults.Values.Any(access => !access.Write))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _secretVersionRepository.DeleteManyByIdAsync(ids);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
@@ -29,6 +30,7 @@ public class SecretsController : Controller
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
private readonly ISecretVersionRepository _secretVersionRepository;
|
||||
private readonly ICreateSecretCommand _createSecretCommand;
|
||||
private readonly IUpdateSecretCommand _updateSecretCommand;
|
||||
private readonly IDeleteSecretCommand _deleteSecretCommand;
|
||||
@@ -38,11 +40,13 @@ public class SecretsController : Controller
|
||||
private readonly IUserService _userService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
|
||||
public SecretsController(
|
||||
ICurrentContext currentContext,
|
||||
IProjectRepository projectRepository,
|
||||
ISecretRepository secretRepository,
|
||||
ISecretVersionRepository secretVersionRepository,
|
||||
ICreateSecretCommand createSecretCommand,
|
||||
IUpdateSecretCommand updateSecretCommand,
|
||||
IDeleteSecretCommand deleteSecretCommand,
|
||||
@@ -51,11 +55,13 @@ public class SecretsController : Controller
|
||||
ISecretAccessPoliciesUpdatesQuery secretAccessPoliciesUpdatesQuery,
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IAuthorizationService authorizationService)
|
||||
IAuthorizationService authorizationService,
|
||||
IOrganizationUserRepository organizationUserRepository)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_projectRepository = projectRepository;
|
||||
_secretRepository = secretRepository;
|
||||
_secretVersionRepository = secretVersionRepository;
|
||||
_createSecretCommand = createSecretCommand;
|
||||
_updateSecretCommand = updateSecretCommand;
|
||||
_deleteSecretCommand = deleteSecretCommand;
|
||||
@@ -65,6 +71,7 @@ public class SecretsController : Controller
|
||||
_userService = userService;
|
||||
_eventService = eventService;
|
||||
_authorizationService = authorizationService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
|
||||
}
|
||||
|
||||
@@ -190,6 +197,44 @@ public class SecretsController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
// Create a version record if the value changed
|
||||
if (updateRequest.ValueChanged)
|
||||
{
|
||||
// Store the old value before updating
|
||||
var oldValue = secret.Value;
|
||||
var userId = _userService.GetProperUserId(User)!.Value;
|
||||
Guid? editorServiceAccountId = null;
|
||||
Guid? editorOrganizationUserId = null;
|
||||
|
||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount)
|
||||
{
|
||||
editorServiceAccountId = userId;
|
||||
}
|
||||
else if (_currentContext.IdentityClientType == IdentityClientType.User)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(secret.OrganizationId, userId);
|
||||
if (orgUser != null)
|
||||
{
|
||||
editorOrganizationUserId = orgUser.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
var secretVersion = new SecretVersion
|
||||
{
|
||||
SecretId = id,
|
||||
Value = oldValue,
|
||||
VersionDate = DateTime.UtcNow,
|
||||
EditorServiceAccountId = editorServiceAccountId,
|
||||
EditorOrganizationUserId = editorOrganizationUserId
|
||||
};
|
||||
|
||||
await _secretVersionRepository.CreateAsync(secretVersion);
|
||||
}
|
||||
|
||||
var result = await _updateSecretCommand.UpdateAsync(updatedSecret, accessPoliciesUpdates);
|
||||
await LogSecretEventAsync(secret, EventType.Secret_Edited);
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.SecretsManager.Models.Request;
|
||||
|
||||
public class RestoreSecretVersionRequestModel
|
||||
{
|
||||
[Required]
|
||||
public Guid VersionId { get; set; }
|
||||
}
|
||||
@@ -28,6 +28,8 @@ public class SecretUpdateRequestModel : IValidatableObject
|
||||
|
||||
public SecretAccessPoliciesRequestsModel AccessPoliciesRequests { get; set; }
|
||||
|
||||
public bool ValueChanged { get; set; } = false;
|
||||
|
||||
public Secret ToSecret(Secret secret)
|
||||
{
|
||||
secret.Key = Key;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
|
||||
namespace Bit.Api.SecretsManager.Models.Response;
|
||||
|
||||
public class SecretVersionResponseModel : ResponseModel
|
||||
{
|
||||
private const string _objectName = "secretVersion";
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public Guid SecretId { get; set; }
|
||||
public string Value { get; set; } = string.Empty;
|
||||
public DateTime VersionDate { get; set; }
|
||||
public Guid? EditorServiceAccountId { get; set; }
|
||||
public Guid? EditorOrganizationUserId { get; set; }
|
||||
|
||||
public SecretVersionResponseModel() : base(_objectName) { }
|
||||
|
||||
public SecretVersionResponseModel(SecretVersion secretVersion) : base(_objectName)
|
||||
{
|
||||
Id = secretVersion.Id;
|
||||
SecretId = secretVersion.SecretId;
|
||||
Value = secretVersion.Value;
|
||||
VersionDate = secretVersion.VersionDate;
|
||||
EditorServiceAccountId = secretVersion.EditorServiceAccountId;
|
||||
EditorOrganizationUserId = secretVersion.EditorOrganizationUserId;
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,6 @@ public class Startup
|
||||
services.AddBillingOperations();
|
||||
services.AddReportingServices();
|
||||
services.AddImportServices();
|
||||
services.AddPhishingDomainServices(globalSettings);
|
||||
|
||||
services.AddSendServices();
|
||||
|
||||
@@ -216,7 +215,7 @@ public class Startup
|
||||
config.Conventions.Add(new PublicApiControllersModelConvention());
|
||||
});
|
||||
|
||||
services.AddSwagger(globalSettings, Environment);
|
||||
services.AddSwaggerGen(globalSettings, Environment);
|
||||
Jobs.JobsHostedService.AddJobsServices(services, globalSettings.SelfHosted);
|
||||
services.AddHostedService<Jobs.JobsHostedService>();
|
||||
|
||||
@@ -226,7 +225,8 @@ public class Startup
|
||||
services.AddHostedService<Core.HostedServices.ApplicationCacheHostedService>();
|
||||
}
|
||||
|
||||
// Add Slack / Teams Services for OAuth API requests - if configured
|
||||
// Add Event Integrations services
|
||||
services.AddEventIntegrationsCommandsQueries(globalSettings);
|
||||
services.AddSlackService(globalSettings);
|
||||
services.AddTeamsService(globalSettings);
|
||||
}
|
||||
@@ -292,17 +292,59 @@ public class Startup
|
||||
});
|
||||
|
||||
// Add Swagger
|
||||
// Note that the swagger.json generation is configured in the call to AddSwaggerGen above.
|
||||
if (Environment.IsDevelopment() || globalSettings.SelfHosted)
|
||||
{
|
||||
// adds the middleware to serve the swagger.json while the server is running
|
||||
app.UseSwagger(config =>
|
||||
{
|
||||
config.RouteTemplate = "specs/{documentName}/swagger.json";
|
||||
|
||||
// Remove all Bitwarden cloud servers and only register the local server
|
||||
config.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
|
||||
swaggerDoc.Servers = new List<OpenApiServer>
|
||||
{
|
||||
swaggerDoc.Servers.Clear();
|
||||
swaggerDoc.Servers.Add(new OpenApiServer
|
||||
{
|
||||
new OpenApiServer { Url = globalSettings.BaseServiceUri.Api }
|
||||
Url = globalSettings.BaseServiceUri.Api,
|
||||
});
|
||||
|
||||
swaggerDoc.Components.SecuritySchemes.Clear();
|
||||
swaggerDoc.Components.SecuritySchemes.Add("oauth2-client-credentials", new OpenApiSecurityScheme
|
||||
{
|
||||
Type = SecuritySchemeType.OAuth2,
|
||||
Flows = new OpenApiOAuthFlows
|
||||
{
|
||||
ClientCredentials = new OpenApiOAuthFlow
|
||||
{
|
||||
TokenUrl = new Uri($"{globalSettings.BaseServiceUri.Identity}/connect/token"),
|
||||
Scopes = new Dictionary<string, string>
|
||||
{
|
||||
{ ApiScopes.ApiOrganization, "Organization APIs" }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
swaggerDoc.SecurityRequirements.Clear();
|
||||
swaggerDoc.SecurityRequirements.Add(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "oauth2-client-credentials"
|
||||
}
|
||||
},
|
||||
[ApiScopes.ApiOrganization]
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// adds the middleware to display the web UI
|
||||
app.UseSwaggerUI(config =>
|
||||
{
|
||||
config.DocumentTitle = "Bitwarden API Documentation";
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
using Bit.Api.AdminConsole.Authorization;
|
||||
using Bit.Api.Tools.Authorization;
|
||||
using Bit.Core.Auth.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;
|
||||
using Bit.SharedWeb.Health;
|
||||
using Bit.SharedWeb.Swagger;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
@@ -17,7 +13,10 @@ namespace Bit.Api.Utilities;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static void AddSwagger(this IServiceCollection services, GlobalSettings globalSettings, IWebHostEnvironment environment)
|
||||
/// <summary>
|
||||
/// Configures the generation of swagger.json OpenAPI spec.
|
||||
/// </summary>
|
||||
public static void AddSwaggerGen(this IServiceCollection services, GlobalSettings globalSettings, IWebHostEnvironment environment)
|
||||
{
|
||||
services.AddSwaggerGen(config =>
|
||||
{
|
||||
@@ -36,6 +35,8 @@ public static class ServiceCollectionExtensions
|
||||
organizations tools for managing members, collections, groups, event logs, and policies.
|
||||
If you are looking for the Vault Management API, refer instead to
|
||||
[this document](https://bitwarden.com/help/vault-management-api/).
|
||||
|
||||
**Note:** your authorization must match the server you have selected.
|
||||
""",
|
||||
License = new OpenApiLicense
|
||||
{
|
||||
@@ -46,36 +47,20 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
config.SwaggerDoc("internal", new OpenApiInfo { Title = "Bitwarden Internal API", Version = "latest" });
|
||||
|
||||
config.AddSecurityDefinition("oauth2-client-credentials", new OpenApiSecurityScheme
|
||||
{
|
||||
Type = SecuritySchemeType.OAuth2,
|
||||
Flows = new OpenApiOAuthFlows
|
||||
{
|
||||
ClientCredentials = new OpenApiOAuthFlow
|
||||
{
|
||||
TokenUrl = new Uri($"{globalSettings.BaseServiceUri.Identity}/connect/token"),
|
||||
Scopes = new Dictionary<string, string>
|
||||
{
|
||||
{ ApiScopes.ApiOrganization, "Organization APIs" },
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
// Configure Bitwarden cloud US and EU servers. These will appear in the swagger.json build artifact
|
||||
// used for our help center. These are overwritten with the local server when running in self-hosted
|
||||
// or dev mode (see Api Startup.cs).
|
||||
config.AddSwaggerServerWithSecurity(
|
||||
serverId: "US_server",
|
||||
serverUrl: "https://api.bitwarden.com",
|
||||
identityTokenUrl: "https://identity.bitwarden.com/connect/token",
|
||||
serverDescription: "US server");
|
||||
|
||||
config.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "oauth2-client-credentials"
|
||||
},
|
||||
},
|
||||
new[] { ApiScopes.ApiOrganization }
|
||||
}
|
||||
});
|
||||
config.AddSwaggerServerWithSecurity(
|
||||
serverId: "EU_server",
|
||||
serverUrl: "https://api.bitwarden.eu",
|
||||
identityTokenUrl: "https://identity.bitwarden.eu/connect/token",
|
||||
serverDescription: "EU server");
|
||||
|
||||
config.DescribeAllParametersInCamelCase();
|
||||
// config.UseReferencedDefinitionsForEnums();
|
||||
@@ -114,25 +99,4 @@ public static class ServiceCollectionExtensions
|
||||
// Admin Console authorization handlers
|
||||
services.AddAdminConsoleAuthorizationHandlers();
|
||||
}
|
||||
|
||||
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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -757,15 +757,10 @@ public class CiphersController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
if (cipher.ArchivedDate.HasValue)
|
||||
{
|
||||
throw new BadRequestException("Cannot move an archived item to an organization.");
|
||||
}
|
||||
|
||||
ValidateClientVersionForFido2CredentialSupport(cipher);
|
||||
|
||||
var original = cipher.Clone();
|
||||
await _cipherService.ShareAsync(original, model.Cipher.ToCipher(cipher), new Guid(model.Cipher.OrganizationId),
|
||||
await _cipherService.ShareAsync(original, model.Cipher.ToCipher(cipher, user.Id), new Guid(model.Cipher.OrganizationId),
|
||||
model.CollectionIds.Select(c => new Guid(c)), user.Id, model.Cipher.LastKnownRevisionDate);
|
||||
|
||||
var sharedCipher = await GetByIdAsync(id, user.Id);
|
||||
@@ -1271,11 +1266,6 @@ public class CiphersController : Controller
|
||||
_logger.LogError("Cipher was not encrypted for the current user. CipherId: {CipherId}, CurrentUser: {CurrentUserId}, EncryptedFor: {EncryptedFor}", cipher.Id, userId, cipher.EncryptedFor);
|
||||
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
|
||||
}
|
||||
|
||||
if (cipher.ArchivedDate.HasValue)
|
||||
{
|
||||
throw new BadRequestException("Cannot move archived items to an organization.");
|
||||
}
|
||||
}
|
||||
|
||||
var shareCiphers = new List<(CipherDetails, DateTime?)>();
|
||||
@@ -1288,11 +1278,6 @@ public class CiphersController : Controller
|
||||
|
||||
ValidateClientVersionForFido2CredentialSupport(existingCipher);
|
||||
|
||||
if (existingCipher.ArchivedDate.HasValue)
|
||||
{
|
||||
throw new BadRequestException("Cannot move archived items to an organization.");
|
||||
}
|
||||
|
||||
shareCiphers.Add((cipher.ToCipherDetails(existingCipher), cipher.LastKnownRevisionDate));
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user