mirror of
https://github.com/bitwarden/server
synced 2025-12-17 08:43:27 +00:00
Merge branch 'main' of github.com:bitwarden/server into arch/seeder-api
# Conflicts: # bitwarden-server.sln # util/Seeder/Factories/UserSeeder.cs
This commit is contained in:
@@ -123,3 +123,12 @@ csharp_style_namespace_declarations = file_scoped:warning
|
|||||||
# Switch expression
|
# Switch expression
|
||||||
dotnet_diagnostic.CS8509.severity = error # missing switch case for named enum value
|
dotnet_diagnostic.CS8509.severity = error # missing switch case for named enum value
|
||||||
dotnet_diagnostic.CS8524.severity = none # missing switch case for unnamed enum value
|
dotnet_diagnostic.CS8524.severity = none # missing switch case for unnamed enum value
|
||||||
|
|
||||||
|
# CA2253: Named placeholders should nto be numeric values
|
||||||
|
dotnet_diagnostic.CA2253.severity = suggestion
|
||||||
|
|
||||||
|
# CA2254: Template should be a static expression
|
||||||
|
dotnet_diagnostic.CA2254.severity = warning
|
||||||
|
|
||||||
|
# CA1727: Use PascalCase for named placeholders
|
||||||
|
dotnet_diagnostic.CA1727.severity = suggestion
|
||||||
|
|||||||
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -96,6 +96,9 @@ src/Admin/Views/Tools @bitwarden/team-billing-dev
|
|||||||
# The PushType enum is expected to be editted by anyone without need for Platform review
|
# The PushType enum is expected to be editted by anyone without need for Platform review
|
||||||
src/Core/Platform/Push/PushType.cs
|
src/Core/Platform/Push/PushType.cs
|
||||||
|
|
||||||
|
# SDK
|
||||||
|
util/RustSdk @bitwarden/team-sdk-sme
|
||||||
|
|
||||||
# Multiple owners - DO NOT REMOVE (BRE)
|
# Multiple owners - DO NOT REMOVE (BRE)
|
||||||
**/packages.lock.json
|
**/packages.lock.json
|
||||||
Directory.Build.props
|
Directory.Build.props
|
||||||
|
|||||||
5
.github/renovate.json5
vendored
5
.github/renovate.json5
vendored
@@ -10,6 +10,11 @@
|
|||||||
"nuget",
|
"nuget",
|
||||||
],
|
],
|
||||||
packageRules: [
|
packageRules: [
|
||||||
|
{
|
||||||
|
groupName: "cargo minor",
|
||||||
|
matchManagers: ["cargo"],
|
||||||
|
matchUpdateTypes: ["minor"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
groupName: "dockerfile minor",
|
groupName: "dockerfile minor",
|
||||||
matchManagers: ["dockerfile"],
|
matchManagers: ["dockerfile"],
|
||||||
|
|||||||
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||||
|
|
||||||
- name: Verify format
|
- name: Verify format
|
||||||
run: dotnet format --verify-no-changes
|
run: dotnet format --verify-no-changes
|
||||||
@@ -117,7 +117,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
@@ -269,7 +269,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Scan Docker image
|
- name: Scan Docker image
|
||||||
id: container-scan
|
id: container-scan
|
||||||
uses: anchore/scan-action@2c901ab7378897c01b8efaa2d0c9bf519cc64b9e # v6.2.0
|
uses: anchore/scan-action@f6601287cdb1efc985d6b765bbf99cb4c0ac29d8 # v7.0.0
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.image-tags.outputs.primary_tag }}
|
image: ${{ steps.image-tags.outputs.primary_tag }}
|
||||||
fail-build: false
|
fail-build: false
|
||||||
@@ -299,7 +299,7 @@ jobs:
|
|||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||||
|
|
||||||
- name: Log in to Azure
|
- name: Log in to Azure
|
||||||
uses: bitwarden/gh-actions/azure-login@main
|
uses: bitwarden/gh-actions/azure-login@main
|
||||||
@@ -425,7 +425,7 @@ jobs:
|
|||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
4
.github/workflows/test-database.yml
vendored
4
.github/workflows/test-database.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
|||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||||
|
|
||||||
- name: Restore tools
|
- name: Restore tools
|
||||||
run: dotnet tool restore
|
run: dotnet tool restore
|
||||||
@@ -179,7 +179,7 @@ jobs:
|
|||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -30,7 +30,15 @@ jobs:
|
|||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||||
|
|
||||||
|
- name: Install rust
|
||||||
|
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # stable
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
|
||||||
|
- name: Cache cargo registry
|
||||||
|
uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2.7.7
|
||||||
|
|
||||||
- name: Install rust
|
- name: Install rust
|
||||||
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # stable
|
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # stable
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -234,3 +234,4 @@ bitwarden_license/src/Sso/Sso.zip
|
|||||||
/identity.json
|
/identity.json
|
||||||
/api.json
|
/api.json
|
||||||
/api.public.json
|
/api.public.json
|
||||||
|
.serena/
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
<Version>2025.10.0</Version>
|
<Version>2025.10.1</Version>
|
||||||
|
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
|||||||
@@ -148,22 +148,30 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
}
|
}
|
||||||
else if (organization.IsStripeEnabled())
|
else if (organization.IsStripeEnabled())
|
||||||
{
|
{
|
||||||
var subscription = await _stripeAdapter.SubscriptionGetAsync(organization.GatewaySubscriptionId);
|
var subscription = await _stripeAdapter.SubscriptionGetAsync(organization.GatewaySubscriptionId, new SubscriptionGetOptions
|
||||||
|
{
|
||||||
|
Expand = ["customer"]
|
||||||
|
});
|
||||||
|
|
||||||
if (subscription.Status is StripeConstants.SubscriptionStatus.Canceled or StripeConstants.SubscriptionStatus.IncompleteExpired)
|
if (subscription.Status is StripeConstants.SubscriptionStatus.Canceled or StripeConstants.SubscriptionStatus.IncompleteExpired)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
await _stripeAdapter.CustomerUpdateAsync(subscription.CustomerId, new CustomerUpdateOptions
|
||||||
{
|
{
|
||||||
Coupon = string.Empty,
|
|
||||||
Email = organization.BillingEmail
|
Email = organization.BillingEmail
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (subscription.Customer.Discount?.Coupon != null)
|
||||||
|
{
|
||||||
|
await _stripeAdapter.CustomerDeleteDiscountAsync(subscription.CustomerId);
|
||||||
|
}
|
||||||
|
|
||||||
await _stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, new SubscriptionUpdateOptions
|
await _stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, new SubscriptionUpdateOptions
|
||||||
{
|
{
|
||||||
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
|
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
|
||||||
DaysUntilDue = 30
|
DaysUntilDue = 30,
|
||||||
});
|
});
|
||||||
|
|
||||||
await _subscriberService.RemovePaymentSource(organization);
|
await _subscriberService.RemovePaymentSource(organization);
|
||||||
|
|||||||
@@ -481,7 +481,6 @@ public class ProviderBillingService(
|
|||||||
City = billingAddress.City,
|
City = billingAddress.City,
|
||||||
State = billingAddress.State
|
State = billingAddress.State
|
||||||
},
|
},
|
||||||
Coupon = !string.IsNullOrEmpty(provider.DiscountId) ? provider.DiscountId : null,
|
|
||||||
Description = provider.DisplayBusinessName(),
|
Description = provider.DisplayBusinessName(),
|
||||||
Email = provider.BillingEmail,
|
Email = provider.BillingEmail,
|
||||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||||
@@ -663,6 +662,7 @@ public class ProviderBillingService(
|
|||||||
: CollectionMethod.SendInvoice,
|
: CollectionMethod.SendInvoice,
|
||||||
Customer = customer.Id,
|
Customer = customer.Id,
|
||||||
DaysUntilDue = usePaymentMethod ? null : 30,
|
DaysUntilDue = usePaymentMethod ? null : 30,
|
||||||
|
Discounts = !string.IsNullOrEmpty(provider.DiscountId) ? [new SubscriptionDiscountOptions { Coupon = provider.DiscountId }] : null,
|
||||||
Items = subscriptionItemOptionsList,
|
Items = subscriptionItemOptionsList,
|
||||||
Metadata = new Dictionary<string, string> { { "providerId", provider.Id.ToString() } },
|
Metadata = new Dictionary<string, string> { { "providerId", provider.Id.ToString() } },
|
||||||
OffSession = true,
|
OffSession = true,
|
||||||
@@ -671,7 +671,6 @@ public class ProviderBillingService(
|
|||||||
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||||
|
|||||||
@@ -157,6 +157,6 @@ public class Startup
|
|||||||
app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
|
app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
|
||||||
|
|
||||||
// Log startup
|
// Log startup
|
||||||
logger.LogInformation(Constants.BypassFiltersEventId, globalSettings.ProjectName + " started.");
|
logger.LogInformation(Constants.BypassFiltersEventId, "{Project} started.", globalSettings.ProjectName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,16 +156,18 @@ public class RemoveOrganizationFromProviderCommandTests
|
|||||||
"b@example.com"
|
"b@example.com"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
|
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId, Arg.Is<SubscriptionGetOptions>(
|
||||||
.Returns(GetSubscription(organization.GatewaySubscriptionId));
|
options => options.Expand.Contains("customer")))
|
||||||
|
.Returns(GetSubscription(organization.GatewaySubscriptionId, organization.GatewayCustomerId));
|
||||||
|
|
||||||
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
|
||||||
await stripeAdapter.Received(1).CustomerUpdateAsync(organization.GatewayCustomerId,
|
await stripeAdapter.Received(1).CustomerUpdateAsync(organization.GatewayCustomerId,
|
||||||
Arg.Is<CustomerUpdateOptions>(options =>
|
Arg.Is<CustomerUpdateOptions>(options => options.Email == "a@example.com"));
|
||||||
options.Coupon == string.Empty && options.Email == "a@example.com"));
|
|
||||||
|
await stripeAdapter.Received(1).CustomerDeleteDiscountAsync(organization.GatewayCustomerId);
|
||||||
|
|
||||||
await stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
|
await stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
|
||||||
Arg.Is<SubscriptionUpdateOptions>(options =>
|
Arg.Is<SubscriptionUpdateOptions>(options =>
|
||||||
@@ -368,10 +370,21 @@ public class RemoveOrganizationFromProviderCommandTests
|
|||||||
Arg.Is<IEnumerable<string>>(emails => emails.FirstOrDefault() == "a@example.com"));
|
Arg.Is<IEnumerable<string>>(emails => emails.FirstOrDefault() == "a@example.com"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Subscription GetSubscription(string subscriptionId) =>
|
private static Subscription GetSubscription(string subscriptionId, string customerId) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Id = subscriptionId,
|
Id = subscriptionId,
|
||||||
|
CustomerId = customerId,
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
Discount = new Discount
|
||||||
|
{
|
||||||
|
Coupon = new Coupon
|
||||||
|
{
|
||||||
|
Id = "coupon-id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
Status = StripeConstants.SubscriptionStatus.Active,
|
Status = StripeConstants.SubscriptionStatus.Active,
|
||||||
Items = new StripeList<SubscriptionItem>
|
Items = new StripeList<SubscriptionItem>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -436,7 +436,7 @@ public class PatchGroupCommandTests
|
|||||||
await sutProvider.GetDependency<IGroupService>().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default);
|
await sutProvider.GetDependency<IGroupService>().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default);
|
||||||
|
|
||||||
// Assert: logging
|
// Assert: logging
|
||||||
sutProvider.GetDependency<ILogger<PatchGroupCommand>>().ReceivedWithAnyArgs().LogWarning(default);
|
sutProvider.GetDependency<ILogger<PatchGroupCommand>>().ReceivedWithAnyArgs().LogWarning("");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|||||||
@@ -472,6 +472,7 @@ public class OrganizationsController : Controller
|
|||||||
organization.UseRiskInsights = model.UseRiskInsights;
|
organization.UseRiskInsights = model.UseRiskInsights;
|
||||||
organization.UseOrganizationDomains = model.UseOrganizationDomains;
|
organization.UseOrganizationDomains = model.UseOrganizationDomains;
|
||||||
organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies;
|
organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies;
|
||||||
|
organization.UseAutomaticUserConfirmation = model.UseAutomaticUserConfirmation;
|
||||||
|
|
||||||
//secrets
|
//secrets
|
||||||
organization.SmSeats = model.SmSeats;
|
organization.SmSeats = model.SmSeats;
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ public class OrganizationEditModel : OrganizationViewModel
|
|||||||
SmServiceAccounts = org.SmServiceAccounts;
|
SmServiceAccounts = org.SmServiceAccounts;
|
||||||
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
|
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
|
||||||
UseOrganizationDomains = org.UseOrganizationDomains;
|
UseOrganizationDomains = org.UseOrganizationDomains;
|
||||||
|
UseAutomaticUserConfirmation = org.UseAutomaticUserConfirmation;
|
||||||
|
|
||||||
_plans = plans;
|
_plans = plans;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +194,8 @@ public class OrganizationEditModel : OrganizationViewModel
|
|||||||
[Display(Name = "Use Organization Domains")]
|
[Display(Name = "Use Organization Domains")]
|
||||||
public bool UseOrganizationDomains { get; set; }
|
public bool UseOrganizationDomains { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Automatic User Confirmation")]
|
||||||
|
public bool UseAutomaticUserConfirmation { get; set; }
|
||||||
/**
|
/**
|
||||||
* Creates a Plan[] object for use in Javascript
|
* Creates a Plan[] object for use in Javascript
|
||||||
* This is mapped manually below to provide some type safety in case the plan objects change
|
* This is mapped manually below to provide some type safety in case the plan objects change
|
||||||
@@ -231,6 +235,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
|||||||
LegacyYear = p.LegacyYear,
|
LegacyYear = p.LegacyYear,
|
||||||
Disabled = p.Disabled,
|
Disabled = p.Disabled,
|
||||||
SupportsSecretsManager = p.SupportsSecretsManager,
|
SupportsSecretsManager = p.SupportsSecretsManager,
|
||||||
|
AutomaticUserConfirmation = p.AutomaticUserConfirmation,
|
||||||
PasswordManager =
|
PasswordManager =
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -159,6 +159,13 @@
|
|||||||
<label class="form-check-label" asp-for="UseAdminSponsoredFamilies"></label>
|
<label class="form-check-label" asp-for="UseAdminSponsoredFamilies"></label>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@if(FeatureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
|
||||||
|
{
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" asp-for="UseAutomaticUserConfirmation" disabled='@(canEditPlan ? null : "disabled")'>
|
||||||
|
<label class="form-check-label" asp-for="UseAutomaticUserConfirmation"></label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
<h3>Password Manager</h3>
|
<h3>Password Manager</h3>
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
using Bit.Admin.Billing.Models;
|
|
||||||
using Bit.Admin.Enums;
|
|
||||||
using Bit.Admin.Utilities;
|
|
||||||
using Bit.Core.Billing.Providers.Migration.Models;
|
|
||||||
using Bit.Core.Billing.Providers.Migration.Services;
|
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace Bit.Admin.Billing.Controllers;
|
|
||||||
|
|
||||||
[Authorize]
|
|
||||||
[Route("migrate-providers")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public class MigrateProvidersController(
|
|
||||||
IProviderMigrator providerMigrator) : Controller
|
|
||||||
{
|
|
||||||
[HttpGet]
|
|
||||||
[RequirePermission(Permission.Tools_MigrateProviders)]
|
|
||||||
public IActionResult Index()
|
|
||||||
{
|
|
||||||
return View(new MigrateProvidersRequestModel());
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[RequirePermission(Permission.Tools_MigrateProviders)]
|
|
||||||
[ValidateAntiForgeryToken]
|
|
||||||
public async Task<IActionResult> PostAsync(MigrateProvidersRequestModel request)
|
|
||||||
{
|
|
||||||
var providerIds = GetProviderIdsFromInput(request.ProviderIds);
|
|
||||||
|
|
||||||
if (providerIds.Count == 0)
|
|
||||||
{
|
|
||||||
return RedirectToAction("Index");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var providerId in providerIds)
|
|
||||||
{
|
|
||||||
await providerMigrator.Migrate(providerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return RedirectToAction("Results", new { ProviderIds = string.Join("\r\n", providerIds) });
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("results")]
|
|
||||||
[RequirePermission(Permission.Tools_MigrateProviders)]
|
|
||||||
public async Task<IActionResult> ResultsAsync(MigrateProvidersRequestModel request)
|
|
||||||
{
|
|
||||||
var providerIds = GetProviderIdsFromInput(request.ProviderIds);
|
|
||||||
|
|
||||||
if (providerIds.Count == 0)
|
|
||||||
{
|
|
||||||
return View(Array.Empty<ProviderMigrationResult>());
|
|
||||||
}
|
|
||||||
|
|
||||||
var results = await Task.WhenAll(providerIds.Select(providerMigrator.GetResult));
|
|
||||||
|
|
||||||
return View(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("results/{providerId:guid}")]
|
|
||||||
[RequirePermission(Permission.Tools_MigrateProviders)]
|
|
||||||
public async Task<IActionResult> DetailsAsync([FromRoute] Guid providerId)
|
|
||||||
{
|
|
||||||
var result = await providerMigrator.GetResult(providerId);
|
|
||||||
|
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
return RedirectToAction("Index");
|
|
||||||
}
|
|
||||||
|
|
||||||
return View(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<Guid> GetProviderIdsFromInput(string text) => !string.IsNullOrEmpty(text)
|
|
||||||
? text.Split(
|
|
||||||
["\r\n", "\r", "\n"],
|
|
||||||
StringSplitOptions.TrimEntries
|
|
||||||
)
|
|
||||||
.Select(id => new Guid(id))
|
|
||||||
.ToList()
|
|
||||||
: [];
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
// FIXME: Update this file to be null safe and then delete the line below
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Bit.Admin.Billing.Models;
|
|
||||||
|
|
||||||
public class MigrateProvidersRequestModel
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
[Display(Name = "Provider IDs")]
|
|
||||||
public string ProviderIds { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
@using System.Text.Json
|
|
||||||
@model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Results";
|
|
||||||
}
|
|
||||||
|
|
||||||
<h1>Migrate Providers</h1>
|
|
||||||
<h2>Migration Details: @Model.ProviderName</h2>
|
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-sm-4 col-lg-3">Id</dt>
|
|
||||||
<dd class="col-sm-8 col-lg-9"><code>@Model.ProviderId</code></dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-4 col-lg-3">Result</dt>
|
|
||||||
<dd class="col-sm-8 col-lg-9">@Model.Result</dd>
|
|
||||||
</dl>
|
|
||||||
<h3>Client Organizations</h3>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-striped table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Result</th>
|
|
||||||
<th>Previous State</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var clientResult in Model.Clients)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td>@clientResult.OrganizationId</td>
|
|
||||||
<td>@clientResult.OrganizationName</td>
|
|
||||||
<td>@clientResult.Result</td>
|
|
||||||
<td><pre>@Html.Raw(JsonSerializer.Serialize(clientResult.PreviousState))</pre></td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
@model Bit.Admin.Billing.Models.MigrateProvidersRequestModel;
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Migrate Providers";
|
|
||||||
}
|
|
||||||
|
|
||||||
<h1>Migrate Providers</h1>
|
|
||||||
<h2>Bulk Consolidated Billing Migration Tool</h2>
|
|
||||||
<section>
|
|
||||||
<p>
|
|
||||||
This tool allows you to provide a list of IDs for Providers that you would like to migrate to Consolidated Billing.
|
|
||||||
Because of the expensive nature of the operation, you can only migrate 10 Providers at a time.
|
|
||||||
</p>
|
|
||||||
<p class="alert alert-warning">
|
|
||||||
Updates made through this tool are irreversible without manual intervention.
|
|
||||||
</p>
|
|
||||||
<p>Example Input (Please enter each Provider ID separated by a new line):</p>
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<pre class="mb-0">f513affc-2290-4336-879e-21ec3ecf3e78
|
|
||||||
f7a5cb0d-4b74-445c-8d8c-232d1d32bbe2
|
|
||||||
bf82d3cf-0e21-4f39-b81b-ef52b2fc6a3a
|
|
||||||
174e82fc-70c3-448d-9fe7-00bad2a3ab00
|
|
||||||
22a4bbbf-58e3-4e4c-a86a-a0d7caf4ff14</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form method="post" asp-controller="MigrateProviders" asp-action="Post" class="mt-2">
|
|
||||||
<div asp-validation-summary="All" class="text-danger"></div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label" asp-for="ProviderIds"></label>
|
|
||||||
<textarea rows="10" class="form-control" type="text" asp-for="ProviderIds"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<input type="submit" value="Run" class="btn btn-primary"/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<form method="get" asp-controller="MigrateProviders" asp-action="Results" class="mt-2">
|
|
||||||
<div asp-validation-summary="All" class="text-danger"></div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label" asp-for="ProviderIds"></label>
|
|
||||||
<textarea rows="10" class="form-control" type="text" asp-for="ProviderIds"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<input type="submit" value="See Previous Results" class="btn btn-primary"/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
@model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult[]
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Results";
|
|
||||||
}
|
|
||||||
|
|
||||||
<h1>Migrate Providers</h1>
|
|
||||||
<h2>Results</h2>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-striped table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Result</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var result in Model)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td><a href="@Url.Action("Details", "MigrateProviders", new { providerId = result.ProviderId })">@result.ProviderId</a></td>
|
|
||||||
<td>@result.ProviderName</td>
|
|
||||||
<td>@result.Result</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
@@ -61,7 +61,7 @@ public class HomeController : Controller
|
|||||||
}
|
}
|
||||||
catch (HttpRequestException e)
|
catch (HttpRequestException e)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, $"Error encountered while sending GET request to {requestUri}");
|
_logger.LogError(e, "Error encountered while sending GET request to {RequestUri}", requestUri);
|
||||||
return new JsonResult("Unable to fetch latest version") { StatusCode = StatusCodes.Status500InternalServerError };
|
return new JsonResult("Unable to fetch latest version") { StatusCode = StatusCodes.Status500InternalServerError };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ public class HomeController : Controller
|
|||||||
}
|
}
|
||||||
catch (HttpRequestException e)
|
catch (HttpRequestException e)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, $"Error encountered while sending GET request to {requestUri}");
|
_logger.LogError(e, "Error encountered while sending GET request to {RequestUri}", requestUri);
|
||||||
return new JsonResult("Unable to fetch installed version") { StatusCode = StatusCodes.Status500InternalServerError };
|
return new JsonResult("Unable to fetch installed version") { StatusCode = StatusCodes.Status500InternalServerError };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// FIXME: Update this file to be null safe and then delete the line below
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Admin.Enums;
|
using Bit.Admin.Enums;
|
||||||
using Bit.Admin.Models;
|
using Bit.Admin.Models;
|
||||||
@@ -10,7 +9,6 @@ using Bit.Core.AdminConsole.Entities;
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Organizations.Queries;
|
using Bit.Core.Billing.Organizations.Queries;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Models.BitStripe;
|
|
||||||
using Bit.Core.Platform.Installations;
|
using Bit.Core.Platform.Installations;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@@ -33,7 +31,6 @@ public class ToolsController : Controller
|
|||||||
private readonly IInstallationRepository _installationRepository;
|
private readonly IInstallationRepository _installationRepository;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IProviderUserRepository _providerUserRepository;
|
private readonly IProviderUserRepository _providerUserRepository;
|
||||||
private readonly IPaymentService _paymentService;
|
|
||||||
private readonly IStripeAdapter _stripeAdapter;
|
private readonly IStripeAdapter _stripeAdapter;
|
||||||
private readonly IWebHostEnvironment _environment;
|
private readonly IWebHostEnvironment _environment;
|
||||||
|
|
||||||
@@ -46,7 +43,6 @@ public class ToolsController : Controller
|
|||||||
IInstallationRepository installationRepository,
|
IInstallationRepository installationRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IProviderUserRepository providerUserRepository,
|
IProviderUserRepository providerUserRepository,
|
||||||
IPaymentService paymentService,
|
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
IWebHostEnvironment environment)
|
IWebHostEnvironment environment)
|
||||||
{
|
{
|
||||||
@@ -58,7 +54,6 @@ public class ToolsController : Controller
|
|||||||
_installationRepository = installationRepository;
|
_installationRepository = installationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_providerUserRepository = providerUserRepository;
|
_providerUserRepository = providerUserRepository;
|
||||||
_paymentService = paymentService;
|
|
||||||
_stripeAdapter = stripeAdapter;
|
_stripeAdapter = stripeAdapter;
|
||||||
_environment = environment;
|
_environment = environment;
|
||||||
}
|
}
|
||||||
@@ -341,138 +336,4 @@ public class ToolsController : Controller
|
|||||||
throw new Exception("No license to generate.");
|
throw new Exception("No license to generate.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequirePermission(Permission.Tools_ManageStripeSubscriptions)]
|
|
||||||
public async Task<IActionResult> StripeSubscriptions(StripeSubscriptionListOptions options)
|
|
||||||
{
|
|
||||||
options = options ?? new StripeSubscriptionListOptions();
|
|
||||||
options.Limit = 10;
|
|
||||||
options.Expand = new List<string>() { "data.customer", "data.latest_invoice" };
|
|
||||||
options.SelectAll = false;
|
|
||||||
|
|
||||||
var subscriptions = await _stripeAdapter.SubscriptionListAsync(options);
|
|
||||||
|
|
||||||
options.StartingAfter = subscriptions.LastOrDefault()?.Id;
|
|
||||||
options.EndingBefore = await StripeSubscriptionsGetHasPreviousPage(subscriptions, options) ?
|
|
||||||
subscriptions.FirstOrDefault()?.Id :
|
|
||||||
null;
|
|
||||||
|
|
||||||
var isProduction = _environment.IsProduction();
|
|
||||||
var model = new StripeSubscriptionsModel()
|
|
||||||
{
|
|
||||||
Items = subscriptions.Select(s => new StripeSubscriptionRowModel(s)).ToList(),
|
|
||||||
Prices = (await _stripeAdapter.PriceListAsync(new Stripe.PriceListOptions() { Limit = 100 })).Data,
|
|
||||||
TestClocks = isProduction ? new List<Stripe.TestHelpers.TestClock>() : await _stripeAdapter.TestClockListAsync(),
|
|
||||||
Filter = options
|
|
||||||
};
|
|
||||||
return View(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[RequirePermission(Permission.Tools_ManageStripeSubscriptions)]
|
|
||||||
public async Task<IActionResult> StripeSubscriptions([FromForm] StripeSubscriptionsModel model)
|
|
||||||
{
|
|
||||||
if (!ModelState.IsValid)
|
|
||||||
{
|
|
||||||
var isProduction = _environment.IsProduction();
|
|
||||||
model.Prices = (await _stripeAdapter.PriceListAsync(new Stripe.PriceListOptions() { Limit = 100 })).Data;
|
|
||||||
model.TestClocks = isProduction ? new List<Stripe.TestHelpers.TestClock>() : await _stripeAdapter.TestClockListAsync();
|
|
||||||
return View(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.Action == StripeSubscriptionsAction.Export || model.Action == StripeSubscriptionsAction.BulkCancel)
|
|
||||||
{
|
|
||||||
var subscriptions = model.Filter.SelectAll ?
|
|
||||||
await _stripeAdapter.SubscriptionListAsync(model.Filter) :
|
|
||||||
model.Items.Where(x => x.Selected).Select(x => x.Subscription);
|
|
||||||
|
|
||||||
if (model.Action == StripeSubscriptionsAction.Export)
|
|
||||||
{
|
|
||||||
return StripeSubscriptionsExport(subscriptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.Action == StripeSubscriptionsAction.BulkCancel)
|
|
||||||
{
|
|
||||||
await StripeSubscriptionsCancel(subscriptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (model.Action == StripeSubscriptionsAction.PreviousPage || model.Action == StripeSubscriptionsAction.Search)
|
|
||||||
{
|
|
||||||
model.Filter.StartingAfter = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.Action == StripeSubscriptionsAction.NextPage || model.Action == StripeSubscriptionsAction.Search)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(model.Filter.StartingAfter))
|
|
||||||
{
|
|
||||||
var subscription = await _stripeAdapter.SubscriptionGetAsync(model.Filter.StartingAfter);
|
|
||||||
if (subscription.Status == "canceled")
|
|
||||||
{
|
|
||||||
model.Filter.StartingAfter = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
model.Filter.EndingBefore = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return RedirectToAction("StripeSubscriptions", model.Filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This requires a redundant API call to Stripe because of the way they handle pagination.
|
|
||||||
// The StartingBefore value has to be inferred from the list we get, and isn't supplied by Stripe.
|
|
||||||
private async Task<bool> StripeSubscriptionsGetHasPreviousPage(List<Stripe.Subscription> subscriptions, StripeSubscriptionListOptions options)
|
|
||||||
{
|
|
||||||
var hasPreviousPage = false;
|
|
||||||
if (subscriptions.FirstOrDefault()?.Id != null)
|
|
||||||
{
|
|
||||||
var previousPageSearchOptions = new StripeSubscriptionListOptions()
|
|
||||||
{
|
|
||||||
EndingBefore = subscriptions.FirstOrDefault().Id,
|
|
||||||
Limit = 1,
|
|
||||||
Status = options.Status,
|
|
||||||
CurrentPeriodEndDate = options.CurrentPeriodEndDate,
|
|
||||||
CurrentPeriodEndRange = options.CurrentPeriodEndRange,
|
|
||||||
Price = options.Price
|
|
||||||
};
|
|
||||||
hasPreviousPage = (await _stripeAdapter.SubscriptionListAsync(previousPageSearchOptions)).Count > 0;
|
|
||||||
}
|
|
||||||
return hasPreviousPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StripeSubscriptionsCancel(IEnumerable<Stripe.Subscription> subscriptions)
|
|
||||||
{
|
|
||||||
foreach (var s in subscriptions)
|
|
||||||
{
|
|
||||||
await _stripeAdapter.SubscriptionCancelAsync(s.Id);
|
|
||||||
if (s.LatestInvoice?.Status == "open")
|
|
||||||
{
|
|
||||||
await _stripeAdapter.InvoiceVoidInvoiceAsync(s.LatestInvoiceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private FileResult StripeSubscriptionsExport(IEnumerable<Stripe.Subscription> subscriptions)
|
|
||||||
{
|
|
||||||
var fieldsToExport = subscriptions.Select(s => new
|
|
||||||
{
|
|
||||||
StripeId = s.Id,
|
|
||||||
CustomerEmail = s.Customer?.Email,
|
|
||||||
SubscriptionStatus = s.Status,
|
|
||||||
InvoiceDueDate = s.CurrentPeriodEnd,
|
|
||||||
SubscriptionProducts = s.Items?.Data.Select(p => p.Plan.Id)
|
|
||||||
});
|
|
||||||
|
|
||||||
var options = new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
WriteIndented = true
|
|
||||||
};
|
|
||||||
|
|
||||||
var result = System.Text.Json.JsonSerializer.Serialize(fieldsToExport, options);
|
|
||||||
var bytes = Encoding.UTF8.GetBytes(result);
|
|
||||||
return File(bytes, "application/json", "StripeSubscriptionsSearch.json");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,8 +52,6 @@ public enum Permission
|
|||||||
Tools_PromoteProviderServiceUser,
|
Tools_PromoteProviderServiceUser,
|
||||||
Tools_GenerateLicenseFile,
|
Tools_GenerateLicenseFile,
|
||||||
Tools_ManageTaxRates,
|
Tools_ManageTaxRates,
|
||||||
Tools_ManageStripeSubscriptions,
|
|
||||||
Tools_CreateEditTransaction,
|
Tools_CreateEditTransaction,
|
||||||
Tools_ProcessStripeEvents,
|
Tools_ProcessStripeEvents
|
||||||
Tools_MigrateProviders
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class AliveJob : BaseJob
|
|||||||
{
|
{
|
||||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Execute job task: Keep alive");
|
_logger.LogInformation(Constants.BypassFiltersEventId, "Execute job task: Keep alive");
|
||||||
var response = await _httpClient.GetAsync(_globalSettings.BaseServiceUri.Admin);
|
var response = await _httpClient.GetAsync(_globalSettings.BaseServiceUri.Admin);
|
||||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Finished job task: Keep alive, " +
|
_logger.LogInformation(Constants.BypassFiltersEventId, "Finished job task: Keep alive, {StatusCode}",
|
||||||
response.StatusCode);
|
response.StatusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +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.Models.BitStripe;
|
|
||||||
|
|
||||||
namespace Bit.Admin.Models;
|
|
||||||
|
|
||||||
public class StripeSubscriptionRowModel
|
|
||||||
{
|
|
||||||
public Stripe.Subscription Subscription { get; set; }
|
|
||||||
public bool Selected { get; set; }
|
|
||||||
|
|
||||||
public StripeSubscriptionRowModel() { }
|
|
||||||
public StripeSubscriptionRowModel(Stripe.Subscription subscription)
|
|
||||||
{
|
|
||||||
Subscription = subscription;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum StripeSubscriptionsAction
|
|
||||||
{
|
|
||||||
Search,
|
|
||||||
PreviousPage,
|
|
||||||
NextPage,
|
|
||||||
Export,
|
|
||||||
BulkCancel
|
|
||||||
}
|
|
||||||
|
|
||||||
public class StripeSubscriptionsModel : IValidatableObject
|
|
||||||
{
|
|
||||||
public List<StripeSubscriptionRowModel> Items { get; set; }
|
|
||||||
public StripeSubscriptionsAction Action { get; set; } = StripeSubscriptionsAction.Search;
|
|
||||||
public string Message { get; set; }
|
|
||||||
public List<Stripe.Price> Prices { get; set; }
|
|
||||||
public List<Stripe.TestHelpers.TestClock> TestClocks { get; set; }
|
|
||||||
public StripeSubscriptionListOptions Filter { get; set; } = new StripeSubscriptionListOptions();
|
|
||||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
|
||||||
{
|
|
||||||
if (Action == StripeSubscriptionsAction.BulkCancel && Filter.Status != "unpaid")
|
|
||||||
{
|
|
||||||
yield return new ValidationResult("Bulk cancel is currently only supported for unpaid subscriptions");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Mvc.Razor;
|
|||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Bit.Admin.Services;
|
using Bit.Admin.Services;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Providers.Migration;
|
|
||||||
|
|
||||||
#if !OSS
|
#if !OSS
|
||||||
using Bit.Commercial.Core.Utilities;
|
using Bit.Commercial.Core.Utilities;
|
||||||
@@ -92,7 +91,6 @@ public class Startup
|
|||||||
services.AddDistributedCache(globalSettings);
|
services.AddDistributedCache(globalSettings);
|
||||||
services.AddBillingOperations();
|
services.AddBillingOperations();
|
||||||
services.AddHttpClient();
|
services.AddHttpClient();
|
||||||
services.AddProviderMigration();
|
|
||||||
|
|
||||||
#if OSS
|
#if OSS
|
||||||
services.AddOosServices();
|
services.AddOosServices();
|
||||||
|
|||||||
@@ -52,8 +52,7 @@ public static class RolePermissionMapping
|
|||||||
Permission.Tools_PromoteAdmin,
|
Permission.Tools_PromoteAdmin,
|
||||||
Permission.Tools_PromoteProviderServiceUser,
|
Permission.Tools_PromoteProviderServiceUser,
|
||||||
Permission.Tools_GenerateLicenseFile,
|
Permission.Tools_GenerateLicenseFile,
|
||||||
Permission.Tools_ManageTaxRates,
|
Permission.Tools_ManageTaxRates
|
||||||
Permission.Tools_ManageStripeSubscriptions
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ "admin", new List<Permission>
|
{ "admin", new List<Permission>
|
||||||
@@ -105,7 +104,6 @@ public static class RolePermissionMapping
|
|||||||
Permission.Tools_PromoteProviderServiceUser,
|
Permission.Tools_PromoteProviderServiceUser,
|
||||||
Permission.Tools_GenerateLicenseFile,
|
Permission.Tools_GenerateLicenseFile,
|
||||||
Permission.Tools_ManageTaxRates,
|
Permission.Tools_ManageTaxRates,
|
||||||
Permission.Tools_ManageStripeSubscriptions,
|
|
||||||
Permission.Tools_CreateEditTransaction
|
Permission.Tools_CreateEditTransaction
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -180,10 +178,8 @@ public static class RolePermissionMapping
|
|||||||
Permission.Tools_ChargeBrainTreeCustomer,
|
Permission.Tools_ChargeBrainTreeCustomer,
|
||||||
Permission.Tools_GenerateLicenseFile,
|
Permission.Tools_GenerateLicenseFile,
|
||||||
Permission.Tools_ManageTaxRates,
|
Permission.Tools_ManageTaxRates,
|
||||||
Permission.Tools_ManageStripeSubscriptions,
|
|
||||||
Permission.Tools_CreateEditTransaction,
|
Permission.Tools_CreateEditTransaction,
|
||||||
Permission.Tools_ProcessStripeEvents,
|
Permission.Tools_ProcessStripeEvents
|
||||||
Permission.Tools_MigrateProviders
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ "sales", new List<Permission>
|
{ "sales", new List<Permission>
|
||||||
|
|||||||
@@ -13,12 +13,10 @@
|
|||||||
var canPromoteAdmin = AccessControlService.UserHasPermission(Permission.Tools_PromoteAdmin);
|
var canPromoteAdmin = AccessControlService.UserHasPermission(Permission.Tools_PromoteAdmin);
|
||||||
var canPromoteProviderServiceUser = AccessControlService.UserHasPermission(Permission.Tools_PromoteProviderServiceUser);
|
var canPromoteProviderServiceUser = AccessControlService.UserHasPermission(Permission.Tools_PromoteProviderServiceUser);
|
||||||
var canGenerateLicense = AccessControlService.UserHasPermission(Permission.Tools_GenerateLicenseFile);
|
var canGenerateLicense = AccessControlService.UserHasPermission(Permission.Tools_GenerateLicenseFile);
|
||||||
var canManageStripeSubscriptions = AccessControlService.UserHasPermission(Permission.Tools_ManageStripeSubscriptions);
|
|
||||||
var canProcessStripeEvents = AccessControlService.UserHasPermission(Permission.Tools_ProcessStripeEvents);
|
var canProcessStripeEvents = AccessControlService.UserHasPermission(Permission.Tools_ProcessStripeEvents);
|
||||||
var canMigrateProviders = AccessControlService.UserHasPermission(Permission.Tools_MigrateProviders);
|
|
||||||
|
|
||||||
var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin || canPromoteProviderServiceUser ||
|
var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin || canPromoteProviderServiceUser ||
|
||||||
canGenerateLicense || canManageStripeSubscriptions;
|
canGenerateLicense;
|
||||||
}
|
}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -102,12 +100,6 @@
|
|||||||
<a class="dropdown-item" asp-controller="Tools" asp-action="GenerateLicense">
|
<a class="dropdown-item" asp-controller="Tools" asp-action="GenerateLicense">
|
||||||
Generate License
|
Generate License
|
||||||
</a>
|
</a>
|
||||||
}
|
|
||||||
@if (canManageStripeSubscriptions)
|
|
||||||
{
|
|
||||||
<a class="dropdown-item" asp-controller="Tools" asp-action="StripeSubscriptions">
|
|
||||||
Manage Stripe Subscriptions
|
|
||||||
</a>
|
|
||||||
}
|
}
|
||||||
@if (canProcessStripeEvents)
|
@if (canProcessStripeEvents)
|
||||||
{
|
{
|
||||||
@@ -115,12 +107,6 @@
|
|||||||
Process Stripe Events
|
Process Stripe Events
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
@if (canMigrateProviders)
|
|
||||||
{
|
|
||||||
<a class="dropdown-item" asp-controller="MigrateProviders" asp-action="index">
|
|
||||||
Migrate Providers
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,277 +0,0 @@
|
|||||||
@model StripeSubscriptionsModel
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Stripe Subscriptions";
|
|
||||||
}
|
|
||||||
|
|
||||||
@section Scripts {
|
|
||||||
<script>
|
|
||||||
function onRowSelect(selectingPage = false) {
|
|
||||||
let checkboxes = document.getElementsByClassName('row-check');
|
|
||||||
let checkedCheckboxCount = 0;
|
|
||||||
let bulkActions = document.getElementById('bulkActions');
|
|
||||||
|
|
||||||
let selectPage = document.getElementById('selectPage');
|
|
||||||
for(let i = 0; i < checkboxes.length; i++){
|
|
||||||
if((checkboxes[i].checked && !selectingPage) || selectingPage && selectPage.checked) {
|
|
||||||
checkboxes[i].checked = true;
|
|
||||||
checkedCheckboxCount += 1;
|
|
||||||
} else {
|
|
||||||
checkboxes[i].checked = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(checkedCheckboxCount > 0) {
|
|
||||||
bulkActions.classList.remove("d-none");
|
|
||||||
} else {
|
|
||||||
bulkActions.classList.add("d-none");
|
|
||||||
}
|
|
||||||
|
|
||||||
let selectAll = document.getElementById('selectAll');
|
|
||||||
if (checkedCheckboxCount === checkboxes.length) {
|
|
||||||
selectPage.checked = true;
|
|
||||||
selectAll.classList.remove("d-none");
|
|
||||||
|
|
||||||
let selectAllElement = document.getElementById('selectAllElement');
|
|
||||||
selectAllElement.classList.remove('d-none');
|
|
||||||
|
|
||||||
let selectedAllConfirmation = document.getElementById('selectedAllConfirmation');
|
|
||||||
selectedAllConfirmation.classList.add('d-none');
|
|
||||||
} else {
|
|
||||||
selectPage.checked = false;
|
|
||||||
selectAll.classList.add("d-none");
|
|
||||||
let selectAllInput = document.getElementById('selectAllInput');
|
|
||||||
selectAllInput.checked = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSelectAll() {
|
|
||||||
let selectAllInput = document.getElementById('selectAllInput');
|
|
||||||
selectAllInput.checked = true;
|
|
||||||
|
|
||||||
let selectAllElement = document.getElementById('selectAllElement');
|
|
||||||
selectAllElement.classList.add('d-none');
|
|
||||||
|
|
||||||
let selectedAllConfirmation = document.getElementById('selectedAllConfirmation');
|
|
||||||
selectedAllConfirmation.classList.remove('d-none');
|
|
||||||
}
|
|
||||||
|
|
||||||
function exportSelectedSubscriptions() {
|
|
||||||
let selectAll = document.getElementById('selectAll');
|
|
||||||
let httpRequest = new XMLHttpRequest();
|
|
||||||
httpRequest.open("POST");
|
|
||||||
httpRequest.send();
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelSelectedSubscriptions() {
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
}
|
|
||||||
|
|
||||||
<h2>Manage Stripe Subscriptions</h2>
|
|
||||||
@if (!string.IsNullOrWhiteSpace(Model.Message))
|
|
||||||
{
|
|
||||||
<div class="alert alert-success"></div>
|
|
||||||
}
|
|
||||||
<form method="post">
|
|
||||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label" asp-for="Filter.Status">Status</label>
|
|
||||||
<select asp-for="Filter.Status" name="filter.Status" class="form-select">
|
|
||||||
<option asp-selected="Model.Filter.Status == null" value="all">All</option>
|
|
||||||
<option asp-selected='Model.Filter.Status == "active"' value="active">Active</option>
|
|
||||||
<option asp-selected='Model.Filter.Status == "unpaid"' value="unpaid">Unpaid</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label" asp-for="Filter.CurrentPeriodEnd">Current Period End</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-text">
|
|
||||||
<div class="form-check form-check-inline mb-0">
|
|
||||||
<input type="radio" class="form-check-input" asp-for="Filter.CurrentPeriodEndRange" value="lt" id="beforeRadio">
|
|
||||||
<label class="form-check-label me-2" for="beforeRadio">Before</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check form-check-inline mb-0">
|
|
||||||
<input type="radio" class="form-check-input" asp-for="Filter.CurrentPeriodEndRange" value="gt" id="afterRadio">
|
|
||||||
<label class="form-check-label" for="afterRadio">After</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@{
|
|
||||||
var date = @Model.Filter.CurrentPeriodEndDate.HasValue ? @Model.Filter.CurrentPeriodEndDate.Value.ToString("yyyy-MM-dd") : string.Empty;
|
|
||||||
}
|
|
||||||
<input type="date" class="form-control" asp-for="Filter.CurrentPeriodEndDate" name="filter.CurrentPeriodEndDate" value="@date">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label" asp-for="Filter.Price">Price ID</label>
|
|
||||||
<select asp-for="Filter.Price" name="filter.Price" class="form-select">
|
|
||||||
<option asp-selected="Model.Filter.Price == null" value="@null">All</option>
|
|
||||||
@foreach (var price in Model.Prices)
|
|
||||||
{
|
|
||||||
<option asp-selected='@(Model.Filter.Price == price.Id)' value="@price.Id">@price.Id</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label" asp-for="Filter.TestClock">Test Clock</label>
|
|
||||||
<select asp-for="Filter.TestClock" name="filter.TestClock" class="form-select">
|
|
||||||
<option asp-selected="Model.Filter.TestClock == null" value="@null">All</option>
|
|
||||||
@foreach (var clock in Model.TestClocks)
|
|
||||||
{
|
|
||||||
<option asp-selected='@(Model.Filter.TestClock == clock.Id)' value="@clock.Id">@clock.Name</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 text-end">
|
|
||||||
<button type="submit" class="btn btn-primary" title="Search" name="action" asp-for="Action" value="@StripeSubscriptionsAction.Search">
|
|
||||||
<i class="fa fa-search"></i> Search
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr/>
|
|
||||||
<input type="checkbox" class="d-none" name="filter.SelectAll" id="selectAllInput" asp-for="@Model.Filter.SelectAll">
|
|
||||||
<div class="text-center row d-flex justify-content-center">
|
|
||||||
<div id="selectAll" class="d-none col-8">
|
|
||||||
All @Model.Items.Count subscriptions on this page are selected.<br/>
|
|
||||||
<button type="button" id="selectAllElement" class="btn btn-link p-0 pb-1" onclick="onSelectAll()">Click here to select all subscriptions for this search.</button>
|
|
||||||
<span id="selectedAllConfirmation" class="d-none text-body-secondary">
|
|
||||||
<i class="fa fa-check"></i> All subscriptions for this search are selected.
|
|
||||||
</span>
|
|
||||||
<div class="alert alert-warning mt-2" role="alert">
|
|
||||||
Please be aware that bulk operations may take several minutes to complete.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-striped table-hover align-middle">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
<div class="form-check">
|
|
||||||
<input id="selectPage" class="form-check-input" type="checkbox" onchange="onRowSelect(true)">
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th>Id</th>
|
|
||||||
<th>Customer Email</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Product Tier</th>
|
|
||||||
<th>Current Period End</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@if (!Model.Items.Any())
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td colspan="6">No results to list.</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
@for (var i = 0; i < Model.Items.Count; i++)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
@{
|
|
||||||
var i0 = i;
|
|
||||||
}
|
|
||||||
<input type="hidden" asp-for="@Model.Items[i0].Subscription.Id" value="@Model.Items[i].Subscription.Id">
|
|
||||||
<input type="hidden" asp-for="@Model.Items[i0].Subscription.Status" value="@Model.Items[i].Subscription.Status">
|
|
||||||
<input type="hidden" asp-for="@Model.Items[i0].Subscription.CurrentPeriodEnd" value="@Model.Items[i].Subscription.CurrentPeriodEnd">
|
|
||||||
<input type="hidden" asp-for="@Model.Items[i0].Subscription.Customer.Email" value="@Model.Items[i].Subscription.Customer.Email">
|
|
||||||
<input type="hidden" asp-for="@Model.Items[i0].Subscription.LatestInvoice.Status" value="@Model.Items[i].Subscription.LatestInvoice.Status">
|
|
||||||
<input type="hidden" asp-for="@Model.Items[i0].Subscription.LatestInvoice.Id" value="@Model.Items[i].Subscription.LatestInvoice.Id">
|
|
||||||
|
|
||||||
@for (var j = 0; j < Model.Items[i].Subscription.Items.Data.Count; j++)
|
|
||||||
{
|
|
||||||
var i1 = i;
|
|
||||||
var j1 = j;
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
asp-for="@Model.Items[i1].Subscription.Items.Data[j1].Plan.Id"
|
|
||||||
value="@Model.Items[i].Subscription.Items.Data[j].Plan.Id">
|
|
||||||
}
|
|
||||||
<div class="form-check">
|
|
||||||
|
|
||||||
@{
|
|
||||||
var i2 = i;
|
|
||||||
}
|
|
||||||
<input class="form-check-input row-check mt-0" onchange="onRowSelect()" asp-for="@Model.Items[i2].Selected">
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
@Model.Items[i].Subscription.Id
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
@Model.Items[i].Subscription.Customer?.Email
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
@Model.Items[i].Subscription.Status
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
@string.Join(",", Model.Items[i].Subscription.Items.Data.Select(product => product.Plan.Id).ToArray())
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
@Model.Items[i].Subscription.CurrentPeriodEnd.ToShortDateString()
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<nav class="d-inline-flex align-items-center">
|
|
||||||
<ul class="pagination mb-0">
|
|
||||||
@if (!string.IsNullOrWhiteSpace(Model.Filter.EndingBefore))
|
|
||||||
{
|
|
||||||
<input type="hidden" asp-for="@Model.Filter.EndingBefore" value="@Model.Filter.EndingBefore">
|
|
||||||
<li class="page-item">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="page-link"
|
|
||||||
name="action"
|
|
||||||
asp-for="Action"
|
|
||||||
value="@StripeSubscriptionsAction.PreviousPage">
|
|
||||||
Previous
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<li class="page-item disabled">
|
|
||||||
<a class="page-link" href="#" tabindex="-1">Previous</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
@if (!string.IsNullOrWhiteSpace(Model.Filter.StartingAfter))
|
|
||||||
{
|
|
||||||
<input type="hidden" asp-for="@Model.Filter.StartingAfter" value="@Model.Filter.StartingAfter">
|
|
||||||
<li class="page-item">
|
|
||||||
<button class="page-link"
|
|
||||||
type="submit"
|
|
||||||
name="action"
|
|
||||||
asp-for="Action"
|
|
||||||
value="@StripeSubscriptionsAction.NextPage">
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<li class="page-item disabled">
|
|
||||||
<a class="page-link" href="#" tabindex="-1">Next</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
<span id="bulkActions" class="d-none ms-3">
|
|
||||||
<span class="d-inline-flex gap-2">
|
|
||||||
<button type="submit" class="btn btn-primary" name="action" asp-for="Action" value="@StripeSubscriptionsAction.Export">
|
|
||||||
Export
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="btn btn-danger" name="action" asp-for="Action" value="@StripeSubscriptionsAction.BulkCancel">
|
|
||||||
Bulk Cancel
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</nav>
|
|
||||||
</form>
|
|
||||||
@@ -70,6 +70,7 @@ public class OrganizationResponseModel : ResponseModel
|
|||||||
UseRiskInsights = organization.UseRiskInsights;
|
UseRiskInsights = organization.UseRiskInsights;
|
||||||
UseOrganizationDomains = organization.UseOrganizationDomains;
|
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||||
|
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
@@ -118,6 +119,7 @@ public class OrganizationResponseModel : ResponseModel
|
|||||||
public bool UseRiskInsights { get; set; }
|
public bool UseRiskInsights { get; set; }
|
||||||
public bool UseOrganizationDomains { get; set; }
|
public bool UseOrganizationDomains { get; set; }
|
||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
|
public bool UseAutomaticUserConfirmation { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
KeyConnectorUrl = ssoConfigData.KeyConnectorUrl;
|
KeyConnectorUrl = ssoConfigData.KeyConnectorUrl;
|
||||||
SsoMemberDecryptionType = ssoConfigData.MemberDecryptionType;
|
SsoMemberDecryptionType = ssoConfigData.MemberDecryptionType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
@@ -164,4 +166,5 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
public bool IsAdminInitiated { get; set; }
|
public bool IsAdminInitiated { get; set; }
|
||||||
public bool SsoEnabled { get; set; }
|
public bool SsoEnabled { get; set; }
|
||||||
public MemberDecryptionType? SsoMemberDecryptionType { get; set; }
|
public MemberDecryptionType? SsoMemberDecryptionType { get; set; }
|
||||||
|
public bool UseAutomaticUserConfirmation { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,5 +52,6 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
|
|||||||
UseRiskInsights = organization.UseRiskInsights;
|
UseRiskInsights = organization.UseRiskInsights;
|
||||||
UseOrganizationDomains = organization.UseOrganizationDomains;
|
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||||
|
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.KeyManagement.Kdf;
|
using Bit.Core.KeyManagement.Kdf;
|
||||||
|
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||||
using Bit.Core.Models.Api.Response;
|
using Bit.Core.Models.Api.Response;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@@ -40,6 +41,7 @@ public class AccountsController : Controller
|
|||||||
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||||
private readonly ITwoFactorEmailService _twoFactorEmailService;
|
private readonly ITwoFactorEmailService _twoFactorEmailService;
|
||||||
private readonly IChangeKdfCommand _changeKdfCommand;
|
private readonly IChangeKdfCommand _changeKdfCommand;
|
||||||
|
|
||||||
@@ -53,6 +55,7 @@ public class AccountsController : Controller
|
|||||||
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
|
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
|
IUserAccountKeysQuery userAccountKeysQuery,
|
||||||
ITwoFactorEmailService twoFactorEmailService,
|
ITwoFactorEmailService twoFactorEmailService,
|
||||||
IChangeKdfCommand changeKdfCommand
|
IChangeKdfCommand changeKdfCommand
|
||||||
)
|
)
|
||||||
@@ -66,6 +69,7 @@ public class AccountsController : Controller
|
|||||||
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
|
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
|
||||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
|
_userAccountKeysQuery = userAccountKeysQuery;
|
||||||
_twoFactorEmailService = twoFactorEmailService;
|
_twoFactorEmailService = twoFactorEmailService;
|
||||||
_changeKdfCommand = changeKdfCommand;
|
_changeKdfCommand = changeKdfCommand;
|
||||||
}
|
}
|
||||||
@@ -332,7 +336,9 @@ public class AccountsController : Controller
|
|||||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
|
|
||||||
var response = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
|
var accountKeys = await _userAccountKeysQuery.Run(user);
|
||||||
|
|
||||||
|
var response = new ProfileResponseModel(user, accountKeys, organizationUserDetails, providerUserDetails,
|
||||||
providerUserOrganizationDetails, twoFactorEnabled,
|
providerUserOrganizationDetails, twoFactorEnabled,
|
||||||
hasPremiumFromOrg, organizationIdsClaimingActiveUser);
|
hasPremiumFromOrg, organizationIdsClaimingActiveUser);
|
||||||
return response;
|
return response;
|
||||||
@@ -364,8 +370,9 @@ public class AccountsController : Controller
|
|||||||
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
|
var userAccountKeys = await _userAccountKeysQuery.Run(user);
|
||||||
|
|
||||||
var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsClaimingActiveUser);
|
var response = new ProfileResponseModel(user, userAccountKeys, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsClaimingActiveUser);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,8 +396,9 @@ public class AccountsController : Controller
|
|||||||
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
|
var accountKeys = await _userAccountKeysQuery.Run(user);
|
||||||
|
|
||||||
var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
|
var response = new ProfileResponseModel(user, accountKeys, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
using Bit.Api.Models.Request;
|
using Bit.Api.Models.Request;
|
||||||
using Bit.Api.Models.Request.Accounts;
|
using Bit.Api.Models.Request.Accounts;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
@@ -8,6 +9,7 @@ using Bit.Core.Billing.Models;
|
|||||||
using Bit.Core.Billing.Models.Business;
|
using Bit.Core.Billing.Models.Business;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
@@ -21,7 +23,8 @@ namespace Bit.Api.Billing.Controllers;
|
|||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
public class AccountsController(
|
public class AccountsController(
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
|
IUserAccountKeysQuery userAccountKeysQuery) : Controller
|
||||||
{
|
{
|
||||||
[HttpPost("premium")]
|
[HttpPost("premium")]
|
||||||
public async Task<PaymentResponseModel> PostPremiumAsync(
|
public async Task<PaymentResponseModel> PostPremiumAsync(
|
||||||
@@ -58,8 +61,9 @@ public class AccountsController(
|
|||||||
var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
|
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
|
var accountKeys = await userAccountKeysQuery.Run(user);
|
||||||
|
|
||||||
var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled,
|
var profile = new ProfileResponseModel(user, accountKeys, null, null, null, userTwoFactorEnabled,
|
||||||
userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
|
userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
|
||||||
return new PaymentResponseModel
|
return new PaymentResponseModel
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ public class OrganizationBillingController(
|
|||||||
return Error.NotFound();
|
return Error.NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = OrganizationMetadataResponse.From(metadata);
|
return TypedResults.Ok(metadata);
|
||||||
|
|
||||||
return TypedResults.Ok(response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("history")]
|
[HttpGet("history")]
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ public class ProviderBillingController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var subscription = await stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId,
|
var subscription = await stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId,
|
||||||
new SubscriptionGetOptions { Expand = ["customer.tax_ids", "test_clock"] });
|
new SubscriptionGetOptions { Expand = ["customer.tax_ids", "discounts", "test_clock"] });
|
||||||
|
|
||||||
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Bit.Api.Billing.Attributes;
|
|||||||
using Bit.Api.Billing.Models.Requests.Payment;
|
using Bit.Api.Billing.Models.Requests.Payment;
|
||||||
using Bit.Api.Billing.Models.Requests.Subscriptions;
|
using Bit.Api.Billing.Models.Requests.Subscriptions;
|
||||||
using Bit.Api.Billing.Models.Requirements;
|
using Bit.Api.Billing.Models.Requirements;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Billing.Commands;
|
using Bit.Core.Billing.Commands;
|
||||||
using Bit.Core.Billing.Organizations.Queries;
|
using Bit.Core.Billing.Organizations.Queries;
|
||||||
@@ -25,6 +26,7 @@ public class OrganizationBillingVNextController(
|
|||||||
ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand,
|
ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand,
|
||||||
IGetBillingAddressQuery getBillingAddressQuery,
|
IGetBillingAddressQuery getBillingAddressQuery,
|
||||||
IGetCreditQuery getCreditQuery,
|
IGetCreditQuery getCreditQuery,
|
||||||
|
IGetOrganizationMetadataQuery getOrganizationMetadataQuery,
|
||||||
IGetOrganizationWarningsQuery getOrganizationWarningsQuery,
|
IGetOrganizationWarningsQuery getOrganizationWarningsQuery,
|
||||||
IGetPaymentMethodQuery getPaymentMethodQuery,
|
IGetPaymentMethodQuery getPaymentMethodQuery,
|
||||||
IRestartSubscriptionCommand restartSubscriptionCommand,
|
IRestartSubscriptionCommand restartSubscriptionCommand,
|
||||||
@@ -113,6 +115,23 @@ public class OrganizationBillingVNextController(
|
|||||||
return Handle(result);
|
return Handle(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize<MemberOrProviderRequirement>]
|
||||||
|
[HttpGet("metadata")]
|
||||||
|
[RequireFeature(FeatureFlagKeys.PM25379_UseNewOrganizationMetadataStructure)]
|
||||||
|
[InjectOrganization]
|
||||||
|
public async Task<IResult> GetMetadataAsync(
|
||||||
|
[BindNever] Organization organization)
|
||||||
|
{
|
||||||
|
var metadata = await getOrganizationMetadataQuery.Run(organization);
|
||||||
|
|
||||||
|
if (metadata == null)
|
||||||
|
{
|
||||||
|
return TypedResults.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypedResults.Ok(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
[Authorize<MemberOrProviderRequirement>]
|
[Authorize<MemberOrProviderRequirement>]
|
||||||
[HttpGet("warnings")]
|
[HttpGet("warnings")]
|
||||||
[InjectOrganization]
|
[InjectOrganization]
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
using Bit.Core.Billing.Organizations.Models;
|
|
||||||
|
|
||||||
namespace Bit.Api.Billing.Models.Responses;
|
|
||||||
|
|
||||||
public record OrganizationMetadataResponse(
|
|
||||||
bool IsEligibleForSelfHost,
|
|
||||||
bool IsManaged,
|
|
||||||
bool IsOnSecretsManagerStandalone,
|
|
||||||
bool IsSubscriptionUnpaid,
|
|
||||||
bool HasSubscription,
|
|
||||||
bool HasOpenInvoice,
|
|
||||||
bool IsSubscriptionCanceled,
|
|
||||||
DateTime? InvoiceDueDate,
|
|
||||||
DateTime? InvoiceCreatedDate,
|
|
||||||
DateTime? SubPeriodEndDate,
|
|
||||||
int OrganizationOccupiedSeats)
|
|
||||||
{
|
|
||||||
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
|
|
||||||
=> new(
|
|
||||||
metadata.IsEligibleForSelfHost,
|
|
||||||
metadata.IsManaged,
|
|
||||||
metadata.IsOnSecretsManagerStandalone,
|
|
||||||
metadata.IsSubscriptionUnpaid,
|
|
||||||
metadata.HasSubscription,
|
|
||||||
metadata.HasOpenInvoice,
|
|
||||||
metadata.IsSubscriptionCanceled,
|
|
||||||
metadata.InvoiceDueDate,
|
|
||||||
metadata.InvoiceCreatedDate,
|
|
||||||
metadata.SubPeriodEndDate,
|
|
||||||
metadata.OrganizationOccupiedSeats);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Providers.Models;
|
using Bit.Core.Billing.Providers.Models;
|
||||||
using Bit.Core.Billing.Tax.Models;
|
using Bit.Core.Billing.Tax.Models;
|
||||||
@@ -10,7 +11,7 @@ namespace Bit.Api.Billing.Models.Responses;
|
|||||||
|
|
||||||
public record ProviderSubscriptionResponse(
|
public record ProviderSubscriptionResponse(
|
||||||
string Status,
|
string Status,
|
||||||
DateTime CurrentPeriodEndDate,
|
DateTime? CurrentPeriodEndDate,
|
||||||
decimal? DiscountPercentage,
|
decimal? DiscountPercentage,
|
||||||
string CollectionMethod,
|
string CollectionMethod,
|
||||||
IEnumerable<ProviderPlanResponse> Plans,
|
IEnumerable<ProviderPlanResponse> Plans,
|
||||||
@@ -51,10 +52,12 @@ public record ProviderSubscriptionResponse(
|
|||||||
|
|
||||||
var accountCredit = Convert.ToDecimal(subscription.Customer?.Balance) * -1 / 100;
|
var accountCredit = Convert.ToDecimal(subscription.Customer?.Balance) * -1 / 100;
|
||||||
|
|
||||||
|
var discount = subscription.Customer?.Discount ?? subscription.Discounts?.FirstOrDefault();
|
||||||
|
|
||||||
return new ProviderSubscriptionResponse(
|
return new ProviderSubscriptionResponse(
|
||||||
subscription.Status,
|
subscription.Status,
|
||||||
subscription.CurrentPeriodEnd,
|
subscription.GetCurrentPeriodEnd(),
|
||||||
subscription.Customer?.Discount?.Coupon?.PercentOff,
|
discount?.Coupon?.PercentOff,
|
||||||
subscription.CollectionMethod,
|
subscription.CollectionMethod,
|
||||||
providerPlanResponses,
|
providerPlanResponses,
|
||||||
accountCredit,
|
accountCredit,
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
using Bit.Api.Models.Response;
|
|
||||||
using Bit.Core.Exceptions;
|
|
||||||
using Bit.Core.Repositories;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace Bit.Api.Controllers;
|
|
||||||
|
|
||||||
[Route("users")]
|
|
||||||
[Authorize("Application")]
|
|
||||||
public class UsersController : Controller
|
|
||||||
{
|
|
||||||
private readonly IUserRepository _userRepository;
|
|
||||||
|
|
||||||
public UsersController(
|
|
||||||
IUserRepository userRepository)
|
|
||||||
{
|
|
||||||
_userRepository = userRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}/public-key")]
|
|
||||||
public async Task<UserKeyResponseModel> Get(string id)
|
|
||||||
{
|
|
||||||
var guidId = new Guid(id);
|
|
||||||
var key = await _userRepository.GetPublicKeyAsync(guidId);
|
|
||||||
if (key == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new UserKeyResponseModel(guidId, key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -106,8 +106,7 @@ public class AccountsKeyManagementController : Controller
|
|||||||
{
|
{
|
||||||
OldMasterKeyAuthenticationHash = model.OldMasterKeyAuthenticationHash,
|
OldMasterKeyAuthenticationHash = model.OldMasterKeyAuthenticationHash,
|
||||||
|
|
||||||
UserKeyEncryptedAccountPrivateKey = model.AccountKeys.UserKeyEncryptedAccountPrivateKey,
|
AccountKeys = model.AccountKeys.ToAccountKeysData(),
|
||||||
AccountPublicKey = model.AccountKeys.AccountPublicKey,
|
|
||||||
|
|
||||||
MasterPasswordUnlockData = model.AccountUnlockData.MasterPasswordUnlockData.ToUnlockData(),
|
MasterPasswordUnlockData = model.AccountUnlockData.MasterPasswordUnlockData.ToUnlockData(),
|
||||||
EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.AccountUnlockData.EmergencyAccessUnlockData),
|
EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.AccountUnlockData.EmergencyAccessUnlockData),
|
||||||
|
|||||||
39
src/Api/KeyManagement/Controllers/UsersController.cs
Normal file
39
src/Api/KeyManagement/Controllers/UsersController.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.KeyManagement.Models.Api.Response;
|
||||||
|
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using UserKeyResponseModel = Bit.Api.Models.Response.UserKeyResponseModel;
|
||||||
|
|
||||||
|
|
||||||
|
namespace Bit.Api.KeyManagement.Controllers;
|
||||||
|
|
||||||
|
[Route("users")]
|
||||||
|
[Authorize("Application")]
|
||||||
|
public class UsersController : Controller
|
||||||
|
{
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||||
|
|
||||||
|
public UsersController(IUserRepository userRepository, IUserAccountKeysQuery userAccountKeysQuery)
|
||||||
|
{
|
||||||
|
_userRepository = userRepository;
|
||||||
|
_userAccountKeysQuery = userAccountKeysQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}/public-key")]
|
||||||
|
public async Task<UserKeyResponseModel> GetPublicKeyAsync([FromRoute] Guid id)
|
||||||
|
{
|
||||||
|
var key = await _userRepository.GetPublicKeyAsync(id) ?? throw new NotFoundException();
|
||||||
|
return new UserKeyResponseModel(id, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}/keys")]
|
||||||
|
public async Task<PublicKeysResponseModel> GetAccountKeysAsync([FromRoute] Guid id)
|
||||||
|
{
|
||||||
|
var user = await _userRepository.GetByIdAsync(id) ?? throw new NotFoundException();
|
||||||
|
var accountKeys = await _userAccountKeysQuery.Run(user) ?? throw new NotFoundException("User account keys not found.");
|
||||||
|
return new PublicKeysResponseModel(accountKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#nullable enable
|
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||||
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||||
@@ -7,4 +8,44 @@ public class AccountKeysRequestModel
|
|||||||
{
|
{
|
||||||
[EncryptedString] public required string UserKeyEncryptedAccountPrivateKey { get; set; }
|
[EncryptedString] public required string UserKeyEncryptedAccountPrivateKey { get; set; }
|
||||||
public required string AccountPublicKey { get; set; }
|
public required string AccountPublicKey { get; set; }
|
||||||
|
|
||||||
|
public PublicKeyEncryptionKeyPairRequestModel? PublicKeyEncryptionKeyPair { get; set; }
|
||||||
|
public SignatureKeyPairRequestModel? SignatureKeyPair { get; set; }
|
||||||
|
public SecurityStateModel? SecurityState { get; set; }
|
||||||
|
|
||||||
|
public UserAccountKeysData ToAccountKeysData()
|
||||||
|
{
|
||||||
|
// This will be cleaned up, after a compatibility period, at which point PublicKeyEncryptionKeyPair and SignatureKeyPair will be required.
|
||||||
|
// TODO: https://bitwarden.atlassian.net/browse/PM-23751
|
||||||
|
if (PublicKeyEncryptionKeyPair == null)
|
||||||
|
{
|
||||||
|
return new UserAccountKeysData
|
||||||
|
{
|
||||||
|
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData
|
||||||
|
(
|
||||||
|
UserKeyEncryptedAccountPrivateKey,
|
||||||
|
AccountPublicKey
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (SignatureKeyPair == null || SecurityState == null)
|
||||||
|
{
|
||||||
|
return new UserAccountKeysData
|
||||||
|
{
|
||||||
|
PublicKeyEncryptionKeyPairData = PublicKeyEncryptionKeyPair.ToPublicKeyEncryptionKeyPairData(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new UserAccountKeysData
|
||||||
|
{
|
||||||
|
PublicKeyEncryptionKeyPairData = PublicKeyEncryptionKeyPair.ToPublicKeyEncryptionKeyPairData(),
|
||||||
|
SignatureKeyPairData = SignatureKeyPair.ToSignatureKeyPairData(),
|
||||||
|
SecurityStateData = SecurityState.ToSecurityState()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#nullable enable
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
using Bit.Core.KeyManagement.Models.Data;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||||
|
|
||||||
|
public class PublicKeyEncryptionKeyPairRequestModel
|
||||||
|
{
|
||||||
|
[EncryptedString] public required string WrappedPrivateKey { get; set; }
|
||||||
|
public required string PublicKey { get; set; }
|
||||||
|
public string? SignedPublicKey { get; set; }
|
||||||
|
|
||||||
|
public PublicKeyEncryptionKeyPairData ToPublicKeyEncryptionKeyPairData()
|
||||||
|
{
|
||||||
|
return new PublicKeyEncryptionKeyPairData(
|
||||||
|
WrappedPrivateKey,
|
||||||
|
PublicKey,
|
||||||
|
SignedPublicKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
#nullable enable
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||||
|
|
||||||
|
public class SignatureKeyPairRequestModel
|
||||||
|
{
|
||||||
|
public required string SignatureAlgorithm { get; set; }
|
||||||
|
[EncryptedString] public required string WrappedSigningKey { get; set; }
|
||||||
|
public required string VerifyingKey { get; set; }
|
||||||
|
|
||||||
|
public SignatureKeyPairData ToSignatureKeyPairData()
|
||||||
|
{
|
||||||
|
if (SignatureAlgorithm != "ed25519")
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"Unsupported signature algorithm: {SignatureAlgorithm}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
var algorithm = Core.KeyManagement.Enums.SignatureAlgorithm.Ed25519;
|
||||||
|
|
||||||
|
return new SignatureKeyPairData(
|
||||||
|
algorithm,
|
||||||
|
WrappedSigningKey,
|
||||||
|
VerifyingKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
#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;
|
||||||
using Bit.Api.Auth.Models.Request.Accounts;
|
using Bit.Api.Auth.Models.Request.Accounts;
|
||||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#nullable enable
|
using Bit.Api.Tools.Models.Request;
|
||||||
using Bit.Api.Tools.Models.Request;
|
|
||||||
using Bit.Api.Vault.Models.Request;
|
using Bit.Api.Vault.Models.Request;
|
||||||
|
|
||||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Bit.Api.Models.Request.Organizations;
|
|
||||||
|
|
||||||
public class OrganizationVerifyBankRequestModel
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
[Range(1, 99)]
|
|
||||||
public int? Amount1 { get; set; }
|
|
||||||
[Required]
|
|
||||||
[Range(1, 99)]
|
|
||||||
public int? Amount2 { get; set; }
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,8 @@ using Bit.Api.AdminConsole.Models.Response;
|
|||||||
using Bit.Api.AdminConsole.Models.Response.Providers;
|
using Bit.Api.AdminConsole.Models.Response.Providers;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.KeyManagement.Models.Api.Response;
|
||||||
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
|
||||||
@@ -13,6 +15,7 @@ namespace Bit.Api.Models.Response;
|
|||||||
public class ProfileResponseModel : ResponseModel
|
public class ProfileResponseModel : ResponseModel
|
||||||
{
|
{
|
||||||
public ProfileResponseModel(User user,
|
public ProfileResponseModel(User user,
|
||||||
|
UserAccountKeysData userAccountKeysData,
|
||||||
IEnumerable<OrganizationUserOrganizationDetails> organizationsUserDetails,
|
IEnumerable<OrganizationUserOrganizationDetails> organizationsUserDetails,
|
||||||
IEnumerable<ProviderUserProviderDetails> providerUserDetails,
|
IEnumerable<ProviderUserProviderDetails> providerUserDetails,
|
||||||
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
|
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
|
||||||
@@ -35,6 +38,7 @@ public class ProfileResponseModel : ResponseModel
|
|||||||
TwoFactorEnabled = twoFactorEnabled;
|
TwoFactorEnabled = twoFactorEnabled;
|
||||||
Key = user.Key;
|
Key = user.Key;
|
||||||
PrivateKey = user.PrivateKey;
|
PrivateKey = user.PrivateKey;
|
||||||
|
AccountKeys = userAccountKeysData != null ? new PrivateKeysResponseModel(userAccountKeysData) : null;
|
||||||
SecurityStamp = user.SecurityStamp;
|
SecurityStamp = user.SecurityStamp;
|
||||||
ForcePasswordReset = user.ForcePasswordReset;
|
ForcePasswordReset = user.ForcePasswordReset;
|
||||||
UsesKeyConnector = user.UsesKeyConnector;
|
UsesKeyConnector = user.UsesKeyConnector;
|
||||||
@@ -60,7 +64,9 @@ public class ProfileResponseModel : ResponseModel
|
|||||||
public string Culture { get; set; }
|
public string Culture { get; set; }
|
||||||
public bool TwoFactorEnabled { get; set; }
|
public bool TwoFactorEnabled { get; set; }
|
||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
|
[Obsolete("Use AccountKeys instead.")]
|
||||||
public string PrivateKey { get; set; }
|
public string PrivateKey { get; set; }
|
||||||
|
public PrivateKeysResponseModel AccountKeys { get; set; }
|
||||||
public string SecurityStamp { get; set; }
|
public string SecurityStamp { get; set; }
|
||||||
public bool ForcePasswordReset { get; set; }
|
public bool ForcePasswordReset { get; set; }
|
||||||
public bool UsesKeyConnector { get; set; }
|
public bool UsesKeyConnector { get; set; }
|
||||||
|
|||||||
@@ -326,6 +326,6 @@ public class Startup
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Log startup
|
// Log startup
|
||||||
logger.LogInformation(Constants.BypassFiltersEventId, globalSettings.ProjectName + " started.");
|
logger.LogInformation(Constants.BypassFiltersEventId, "{Project} started.", globalSettings.ProjectName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ public class SendsController : Controller
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, $"Uncaught exception occurred while handling event grid event: {JsonSerializer.Serialize(eventGridEvent)}");
|
_logger.LogError(e, "Uncaught exception occurred while handling event grid event: {Event}", JsonSerializer.Serialize(eventGridEvent));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ public class ExceptionHandlerFilterAttribute : ExceptionFilterAttribute
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ExceptionHandlerFilterAttribute>>();
|
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ExceptionHandlerFilterAttribute>>();
|
||||||
logger.LogError(0, exception, exception.Message);
|
logger.LogError(0, exception, "Unhandled exception");
|
||||||
errorMessage = "An unhandled server error has occurred.";
|
errorMessage = "An unhandled server error has occurred.";
|
||||||
context.HttpContext.Response.StatusCode = 500;
|
context.HttpContext.Response.StatusCode = 500;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -754,6 +754,11 @@ public class CiphersController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cipher.ArchivedDate.HasValue)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot move an archived item to an organization.");
|
||||||
|
}
|
||||||
|
|
||||||
ValidateClientVersionForFido2CredentialSupport(cipher);
|
ValidateClientVersionForFido2CredentialSupport(cipher);
|
||||||
|
|
||||||
var original = cipher.Clone();
|
var original = cipher.Clone();
|
||||||
@@ -1263,6 +1268,11 @@ 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);
|
_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.");
|
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?)>();
|
var shareCiphers = new List<(CipherDetails, DateTime?)>();
|
||||||
@@ -1275,6 +1285,11 @@ public class CiphersController : Controller
|
|||||||
|
|
||||||
ValidateClientVersionForFido2CredentialSupport(existingCipher);
|
ValidateClientVersionForFido2CredentialSupport(existingCipher);
|
||||||
|
|
||||||
|
if (existingCipher.ArchivedDate.HasValue)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot move archived items to an organization.");
|
||||||
|
}
|
||||||
|
|
||||||
shareCiphers.Add((cipher.ToCipherDetails(existingCipher), cipher.LastKnownRevisionDate));
|
shareCiphers.Add((cipher.ToCipherDetails(existingCipher), cipher.LastKnownRevisionDate));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1578,7 +1593,7 @@ public class CiphersController : Controller
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, $"Uncaught exception occurred while handling event grid event: {JsonSerializer.Serialize(eventGridEvent)}");
|
_logger.LogError(e, "Uncaught exception occurred while handling event grid event: {Event}", JsonSerializer.Serialize(eventGridEvent));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ using Bit.Core.Context;
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
|
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@@ -42,6 +44,7 @@ public class SyncController : Controller
|
|||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
|
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||||
|
|
||||||
public SyncController(
|
public SyncController(
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
@@ -57,7 +60,8 @@ public class SyncController : Controller
|
|||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
|
IUserAccountKeysQuery userAccountKeysQuery)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_folderRepository = folderRepository;
|
_folderRepository = folderRepository;
|
||||||
@@ -73,6 +77,7 @@ public class SyncController : Controller
|
|||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
|
_userAccountKeysQuery = userAccountKeysQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
@@ -116,7 +121,14 @@ public class SyncController : Controller
|
|||||||
|
|
||||||
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||||
|
|
||||||
var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities,
|
UserAccountKeysData userAccountKeys = null;
|
||||||
|
// JIT TDE users and some broken/old users may not have a private key.
|
||||||
|
if (!string.IsNullOrWhiteSpace(user.PrivateKey))
|
||||||
|
{
|
||||||
|
userAccountKeys = await _userAccountKeysQuery.Run(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = new SyncResponseModel(_globalSettings, user, userAccountKeys, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities,
|
||||||
organizationIdsClaimingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
|
organizationIdsClaimingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
|
||||||
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
|
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ using Bit.Api.Tools.Models.Response;
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.KeyManagement.Models.Response;
|
using Bit.Core.KeyManagement.Models.Api.Response;
|
||||||
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
@@ -24,6 +25,7 @@ public class SyncResponseModel() : ResponseModel("sync")
|
|||||||
public SyncResponseModel(
|
public SyncResponseModel(
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
User user,
|
User user,
|
||||||
|
UserAccountKeysData userAccountKeysData,
|
||||||
bool userTwoFactorEnabled,
|
bool userTwoFactorEnabled,
|
||||||
bool userHasPremiumFromOrganization,
|
bool userHasPremiumFromOrganization,
|
||||||
IDictionary<Guid, OrganizationAbility> organizationAbilities,
|
IDictionary<Guid, OrganizationAbility> organizationAbilities,
|
||||||
@@ -40,7 +42,7 @@ public class SyncResponseModel() : ResponseModel("sync")
|
|||||||
IEnumerable<Send> sends)
|
IEnumerable<Send> sends)
|
||||||
: this()
|
: this()
|
||||||
{
|
{
|
||||||
Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
|
Profile = new ProfileResponseModel(user, userAccountKeysData, organizationUserDetails, providerUserDetails,
|
||||||
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser);
|
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser);
|
||||||
Folders = folders.Select(f => new FolderResponseModel(f));
|
Folders = folders.Select(f => new FolderResponseModel(f));
|
||||||
Ciphers = ciphers.Select(cipher =>
|
Ciphers = ciphers.Select(cipher =>
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ public class BillingSettings
|
|||||||
{
|
{
|
||||||
public virtual string JobsKey { get; set; }
|
public virtual string JobsKey { get; set; }
|
||||||
public virtual string StripeWebhookKey { get; set; }
|
public virtual string StripeWebhookKey { get; set; }
|
||||||
public virtual string StripeWebhookSecret { get; set; }
|
public virtual string StripeWebhookSecret20250827Basil { get; set; }
|
||||||
public virtual string StripeWebhookSecret20231016 { get; set; }
|
|
||||||
public virtual string StripeWebhookSecret20240620 { get; set; }
|
|
||||||
public virtual string BitPayWebhookKey { get; set; }
|
public virtual string BitPayWebhookKey { get; set; }
|
||||||
public virtual string AppleWebhookKey { get; set; }
|
public virtual string AppleWebhookKey { get; set; }
|
||||||
public virtual FreshDeskSettings FreshDesk { get; set; } = new FreshDeskSettings();
|
public virtual FreshDeskSettings FreshDesk { get; set; } = new FreshDeskSettings();
|
||||||
|
|||||||
@@ -120,9 +120,7 @@ public class StripeController : Controller
|
|||||||
|
|
||||||
return deliveryContainer.ApiVersion switch
|
return deliveryContainer.ApiVersion switch
|
||||||
{
|
{
|
||||||
"2024-06-20" => HandleVersionWith(_billingSettings.StripeWebhookSecret20240620),
|
"2025-08-27.basil" => HandleVersionWith(_billingSettings.StripeWebhookSecret20250827Basil),
|
||||||
"2023-10-16" => HandleVersionWith(_billingSettings.StripeWebhookSecret20231016),
|
|
||||||
"2022-08-01" => HandleVersionWith(_billingSettings.StripeWebhookSecret),
|
|
||||||
_ => HandleDefault(deliveryContainer.ApiVersion)
|
_ => HandleDefault(deliveryContainer.ApiVersion)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Event = Stripe.Event;
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Event = Stripe.Event;
|
||||||
|
|
||||||
namespace Bit.Billing.Services.Implementations;
|
namespace Bit.Billing.Services.Implementations;
|
||||||
|
|
||||||
@@ -35,13 +36,13 @@ public class InvoiceCreatedHandler(
|
|||||||
if (usingPayPal && invoice is
|
if (usingPayPal && invoice is
|
||||||
{
|
{
|
||||||
AmountDue: > 0,
|
AmountDue: > 0,
|
||||||
Paid: false,
|
Status: not StripeConstants.InvoiceStatus.Paid,
|
||||||
CollectionMethod: "charge_automatically",
|
CollectionMethod: "charge_automatically",
|
||||||
BillingReason:
|
BillingReason:
|
||||||
"subscription_create" or
|
"subscription_create" or
|
||||||
"subscription_cycle" or
|
"subscription_cycle" or
|
||||||
"automatic_pending_invoice_item_invoice",
|
"automatic_pending_invoice_item_invoice",
|
||||||
SubscriptionId: not null and not ""
|
Parent.SubscriptionDetails: not null
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
await stripeEventUtilityService.AttemptToPayInvoiceAsync(invoice);
|
await stripeEventUtilityService.AttemptToPayInvoiceAsync(invoice);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Stripe;
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Stripe;
|
||||||
using Event = Stripe.Event;
|
using Event = Stripe.Event;
|
||||||
|
|
||||||
namespace Bit.Billing.Services.Implementations;
|
namespace Bit.Billing.Services.Implementations;
|
||||||
@@ -26,17 +27,20 @@ public class PaymentFailedHandler : IPaymentFailedHandler
|
|||||||
public async Task HandleAsync(Event parsedEvent)
|
public async Task HandleAsync(Event parsedEvent)
|
||||||
{
|
{
|
||||||
var invoice = await _stripeEventService.GetInvoice(parsedEvent, true);
|
var invoice = await _stripeEventService.GetInvoice(parsedEvent, true);
|
||||||
if (invoice.Paid || invoice.AttemptCount <= 1 || !ShouldAttemptToPayInvoice(invoice))
|
if (invoice.Status == StripeConstants.InvoiceStatus.Paid || invoice.AttemptCount <= 1 || !ShouldAttemptToPayInvoice(invoice))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscription = await _stripeFacade.GetSubscription(invoice.SubscriptionId);
|
if (invoice.Parent?.SubscriptionDetails != null)
|
||||||
// attempt count 4 = 11 days after initial failure
|
|
||||||
if (invoice.AttemptCount <= 3 ||
|
|
||||||
!subscription.Items.Any(i => i.Price.Id is IStripeEventUtilityService.PremiumPlanId or IStripeEventUtilityService.PremiumPlanIdAppStore))
|
|
||||||
{
|
{
|
||||||
await _stripeEventUtilityService.AttemptToPayInvoiceAsync(invoice);
|
var subscription = await _stripeFacade.GetSubscription(invoice.Parent.SubscriptionDetails.SubscriptionId);
|
||||||
|
// attempt count 4 = 11 days after initial failure
|
||||||
|
if (invoice.AttemptCount <= 3 ||
|
||||||
|
!subscription.Items.Any(i => i.Price.Id is IStripeEventUtilityService.PremiumPlanId or IStripeEventUtilityService.PremiumPlanIdAppStore))
|
||||||
|
{
|
||||||
|
await _stripeEventUtilityService.AttemptToPayInvoiceAsync(invoice);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,9 +48,9 @@ public class PaymentFailedHandler : IPaymentFailedHandler
|
|||||||
invoice is
|
invoice is
|
||||||
{
|
{
|
||||||
AmountDue: > 0,
|
AmountDue: > 0,
|
||||||
Paid: false,
|
Status: not StripeConstants.InvoiceStatus.Paid,
|
||||||
CollectionMethod: "charge_automatically",
|
CollectionMethod: "charge_automatically",
|
||||||
BillingReason: "subscription_cycle" or "automatic_pending_invoice_item_invoice",
|
BillingReason: "subscription_cycle" or "automatic_pending_invoice_item_invoice",
|
||||||
SubscriptionId: not null
|
Parent.SubscriptionDetails: not null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using Bit.Billing.Constants;
|
using Bit.Billing.Constants;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@@ -29,12 +31,17 @@ public class PaymentSucceededHandler(
|
|||||||
public async Task HandleAsync(Event parsedEvent)
|
public async Task HandleAsync(Event parsedEvent)
|
||||||
{
|
{
|
||||||
var invoice = await stripeEventService.GetInvoice(parsedEvent, true);
|
var invoice = await stripeEventService.GetInvoice(parsedEvent, true);
|
||||||
if (!invoice.Paid || invoice.BillingReason != "subscription_create")
|
if (invoice.Status != StripeConstants.InvoiceStatus.Paid || invoice.BillingReason != "subscription_create")
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscription = await stripeFacade.GetSubscription(invoice.SubscriptionId);
|
if (invoice.Parent?.SubscriptionDetails == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscription = await stripeFacade.GetSubscription(invoice.Parent.SubscriptionDetails.SubscriptionId);
|
||||||
if (subscription?.Status != StripeSubscriptionStatus.Active)
|
if (subscription?.Status != StripeSubscriptionStatus.Active)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -96,7 +103,7 @@ public class PaymentSucceededHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
|
await organizationEnableCommand.EnableAsync(organizationId.Value, subscription.GetCurrentPeriodEnd());
|
||||||
organization = await organizationRepository.GetByIdAsync(organization.Id);
|
organization = await organizationRepository.GetByIdAsync(organization.Id);
|
||||||
await pushNotificationAdapter.NotifyEnabledChangedAsync(organization!);
|
await pushNotificationAdapter.NotifyEnabledChangedAsync(organization!);
|
||||||
}
|
}
|
||||||
@@ -107,7 +114,7 @@ public class PaymentSucceededHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await userService.EnablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd);
|
await userService.EnablePremiumAsync(userId.Value, subscription.GetCurrentPeriodEnd());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,14 @@ public class ProviderEventService(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var invoice = await stripeEventService.GetInvoice(parsedEvent);
|
var invoice = await stripeEventService.GetInvoice(parsedEvent, true, ["discounts"]);
|
||||||
|
|
||||||
var metadata = (await stripeFacade.GetSubscription(invoice.SubscriptionId)).Metadata ?? new Dictionary<string, string>();
|
if (invoice.Parent is not { Type: "subscription_details" })
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata = (await stripeFacade.GetSubscription(invoice.Parent.SubscriptionDetails.SubscriptionId)).Metadata ?? new Dictionary<string, string>();
|
||||||
|
|
||||||
var hasProviderId = metadata.TryGetValue("providerId", out var providerId);
|
var hasProviderId = metadata.TryGetValue("providerId", out var providerId);
|
||||||
|
|
||||||
@@ -68,7 +73,9 @@ public class ProviderEventService(
|
|||||||
|
|
||||||
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||||
|
|
||||||
var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100;
|
var totalPercentOff = invoice.Discounts?.Sum(discount => discount?.Coupon?.PercentOff ?? 0) ?? 0;
|
||||||
|
|
||||||
|
var discountedPercentage = (100 - totalPercentOff) / 100;
|
||||||
|
|
||||||
var discountedSeatPrice = plan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage;
|
var discountedSeatPrice = plan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage;
|
||||||
|
|
||||||
@@ -96,7 +103,9 @@ public class ProviderEventService(
|
|||||||
|
|
||||||
var unassignedSeats = providerPlan.SeatMinimum - clientSeats ?? 0;
|
var unassignedSeats = providerPlan.SeatMinimum - clientSeats ?? 0;
|
||||||
|
|
||||||
var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100;
|
var totalPercentOff = invoice.Discounts?.Sum(discount => discount?.Coupon?.PercentOff ?? 0) ?? 0;
|
||||||
|
|
||||||
|
var discountedPercentage = (100 - totalPercentOff) / 100;
|
||||||
|
|
||||||
var discountedSeatPrice = plan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage;
|
var discountedSeatPrice = plan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using Bit.Billing.Constants;
|
using Bit.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@@ -87,25 +88,6 @@ public class StripeEventUtilityService : IStripeEventUtilityService
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<(Guid?, Guid?, Guid?)> GetEntityIdsFromChargeAsync(Charge charge)
|
public async Task<(Guid?, Guid?, Guid?)> GetEntityIdsFromChargeAsync(Charge charge)
|
||||||
{
|
{
|
||||||
Guid? organizationId = null;
|
|
||||||
Guid? userId = null;
|
|
||||||
Guid? providerId = null;
|
|
||||||
|
|
||||||
if (charge.InvoiceId != null)
|
|
||||||
{
|
|
||||||
var invoice = await _stripeFacade.GetInvoice(charge.InvoiceId);
|
|
||||||
if (invoice?.SubscriptionId != null)
|
|
||||||
{
|
|
||||||
var subscription = await _stripeFacade.GetSubscription(invoice.SubscriptionId);
|
|
||||||
(organizationId, userId, providerId) = GetIdsFromMetadata(subscription?.Metadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (organizationId.HasValue || userId.HasValue || providerId.HasValue)
|
|
||||||
{
|
|
||||||
return (organizationId, userId, providerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var subscriptions = await _stripeFacade.ListSubscriptions(new SubscriptionListOptions
|
var subscriptions = await _stripeFacade.ListSubscriptions(new SubscriptionListOptions
|
||||||
{
|
{
|
||||||
Customer = charge.CustomerId
|
Customer = charge.CustomerId
|
||||||
@@ -118,7 +100,7 @@ public class StripeEventUtilityService : IStripeEventUtilityService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
(organizationId, userId, providerId) = GetIdsFromMetadata(subscription.Metadata);
|
var (organizationId, userId, providerId) = GetIdsFromMetadata(subscription.Metadata);
|
||||||
|
|
||||||
if (organizationId.HasValue || userId.HasValue || providerId.HasValue)
|
if (organizationId.HasValue || userId.HasValue || providerId.HasValue)
|
||||||
{
|
{
|
||||||
@@ -256,10 +238,10 @@ public class StripeEventUtilityService : IStripeEventUtilityService
|
|||||||
invoice is
|
invoice is
|
||||||
{
|
{
|
||||||
AmountDue: > 0,
|
AmountDue: > 0,
|
||||||
Paid: false,
|
Status: not StripeConstants.InvoiceStatus.Paid,
|
||||||
CollectionMethod: "charge_automatically",
|
CollectionMethod: "charge_automatically",
|
||||||
BillingReason: "subscription_cycle" or "automatic_pending_invoice_item_invoice",
|
BillingReason: "subscription_cycle" or "automatic_pending_invoice_item_invoice",
|
||||||
SubscriptionId: not null
|
Parent.SubscriptionDetails: not null
|
||||||
};
|
};
|
||||||
|
|
||||||
private async Task<bool> AttemptToPayInvoiceWithBraintreeAsync(Invoice invoice, Customer customer)
|
private async Task<bool> AttemptToPayInvoiceWithBraintreeAsync(Invoice invoice, Customer customer)
|
||||||
@@ -272,7 +254,13 @@ public class StripeEventUtilityService : IStripeEventUtilityService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscription = await _stripeFacade.GetSubscription(invoice.SubscriptionId);
|
if (invoice.Parent?.SubscriptionDetails == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Invoice parent was not a subscription.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscription = await _stripeFacade.GetSubscription(invoice.Parent.SubscriptionDetails.SubscriptionId);
|
||||||
var (organizationId, userId, providerId) = GetIdsFromMetadata(subscription?.Metadata);
|
var (organizationId, userId, providerId) = GetIdsFromMetadata(subscription?.Metadata);
|
||||||
if (!organizationId.HasValue && !userId.HasValue && !providerId.HasValue)
|
if (!organizationId.HasValue && !userId.HasValue && !providerId.HasValue)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Bit.Billing.Constants;
|
using Bit.Billing.Constants;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Event = Stripe.Event;
|
using Event = Stripe.Event;
|
||||||
namespace Bit.Billing.Services.Implementations;
|
namespace Bit.Billing.Services.Implementations;
|
||||||
@@ -50,11 +51,11 @@ public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
|
await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.GetCurrentPeriodEnd());
|
||||||
}
|
}
|
||||||
else if (userId.HasValue)
|
else if (userId.HasValue)
|
||||||
{
|
{
|
||||||
await _userService.DisablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd);
|
await _userService.DisablePremiumAsync(userId.Value, subscription.GetCurrentPeriodEnd());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ using Bit.Core;
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@@ -82,12 +84,14 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
|||||||
var subscription = await _stripeEventService.GetSubscription(parsedEvent, true, ["customer", "discounts", "latest_invoice", "test_clock"]);
|
var subscription = await _stripeEventService.GetSubscription(parsedEvent, true, ["customer", "discounts", "latest_invoice", "test_clock"]);
|
||||||
var (organizationId, userId, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
|
var (organizationId, userId, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
|
||||||
|
|
||||||
|
var currentPeriodEnd = subscription.GetCurrentPeriodEnd();
|
||||||
|
|
||||||
switch (subscription.Status)
|
switch (subscription.Status)
|
||||||
{
|
{
|
||||||
case StripeSubscriptionStatus.Unpaid or StripeSubscriptionStatus.IncompleteExpired
|
case StripeSubscriptionStatus.Unpaid or StripeSubscriptionStatus.IncompleteExpired
|
||||||
when organizationId.HasValue:
|
when organizationId.HasValue:
|
||||||
{
|
{
|
||||||
await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
|
await _organizationDisableCommand.DisableAsync(organizationId.Value, currentPeriodEnd);
|
||||||
if (subscription.Status == StripeSubscriptionStatus.Unpaid &&
|
if (subscription.Status == StripeSubscriptionStatus.Unpaid &&
|
||||||
subscription.LatestInvoice is { BillingReason: "subscription_cycle" or "subscription_create" })
|
subscription.LatestInvoice is { BillingReason: "subscription_cycle" or "subscription_create" })
|
||||||
{
|
{
|
||||||
@@ -114,7 +118,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
|||||||
await VoidOpenInvoices(subscription.Id);
|
await VoidOpenInvoices(subscription.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _userService.DisablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd);
|
await _userService.DisablePremiumAsync(userId.Value, currentPeriodEnd);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -154,7 +158,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
|||||||
{
|
{
|
||||||
if (userId.HasValue)
|
if (userId.HasValue)
|
||||||
{
|
{
|
||||||
await _userService.EnablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd);
|
await _userService.EnablePremiumAsync(userId.Value, currentPeriodEnd);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -162,17 +166,17 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
|||||||
|
|
||||||
if (organizationId.HasValue)
|
if (organizationId.HasValue)
|
||||||
{
|
{
|
||||||
await _organizationService.UpdateExpirationDateAsync(organizationId.Value, subscription.CurrentPeriodEnd);
|
await _organizationService.UpdateExpirationDateAsync(organizationId.Value, currentPeriodEnd);
|
||||||
if (_stripeEventUtilityService.IsSponsoredSubscription(subscription))
|
if (_stripeEventUtilityService.IsSponsoredSubscription(subscription) && currentPeriodEnd.HasValue)
|
||||||
{
|
{
|
||||||
await _organizationSponsorshipRenewCommand.UpdateExpirationDateAsync(organizationId.Value, subscription.CurrentPeriodEnd);
|
await _organizationSponsorshipRenewCommand.UpdateExpirationDateAsync(organizationId.Value, currentPeriodEnd.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
await RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync(parsedEvent, subscription);
|
await RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync(parsedEvent, subscription);
|
||||||
}
|
}
|
||||||
else if (userId.HasValue)
|
else if (userId.HasValue)
|
||||||
{
|
{
|
||||||
await _userService.UpdatePremiumExpirationAsync(userId.Value, subscription.CurrentPeriodEnd);
|
await _userService.UpdatePremiumExpirationAsync(userId.Value, currentPeriodEnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,9 +284,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
|||||||
?.Coupon
|
?.Coupon
|
||||||
?.Id == "sm-standalone";
|
?.Id == "sm-standalone";
|
||||||
|
|
||||||
var subscriptionHasSecretsManagerTrial = subscription.Discount
|
var subscriptionHasSecretsManagerTrial = subscription.Discounts.Select(discount => discount.Coupon.Id)
|
||||||
?.Coupon
|
.Contains(StripeConstants.CouponIDs.SecretsManagerStandalone);
|
||||||
?.Id == "sm-standalone";
|
|
||||||
|
|
||||||
if (customerHasSecretsManagerTrial)
|
if (customerHasSecretsManagerTrial)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,17 +36,16 @@ public class UpcomingInvoiceHandler(
|
|||||||
{
|
{
|
||||||
var invoice = await stripeEventService.GetInvoice(parsedEvent);
|
var invoice = await stripeEventService.GetInvoice(parsedEvent);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(invoice.SubscriptionId))
|
var customer =
|
||||||
|
await stripeFacade.GetCustomer(invoice.CustomerId, new CustomerGetOptions { Expand = ["subscriptions", "tax", "tax_ids"] });
|
||||||
|
|
||||||
|
var subscription = customer.Subscriptions.FirstOrDefault();
|
||||||
|
|
||||||
|
if (subscription == null)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Received 'invoice.upcoming' Event with ID '{eventId}' that did not include a Subscription ID", parsedEvent.Id);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscription = await stripeFacade.GetSubscription(invoice.SubscriptionId, new SubscriptionGetOptions
|
|
||||||
{
|
|
||||||
Expand = ["customer.tax", "customer.tax_ids"]
|
|
||||||
});
|
|
||||||
|
|
||||||
var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
|
var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
|
||||||
|
|
||||||
if (organizationId.HasValue)
|
if (organizationId.HasValue)
|
||||||
@@ -58,7 +57,7 @@ public class UpcomingInvoiceHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await AlignOrganizationTaxConcernsAsync(organization, subscription, parsedEvent.Id);
|
await AlignOrganizationTaxConcernsAsync(organization, subscription, customer, parsedEvent.Id);
|
||||||
|
|
||||||
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||||
|
|
||||||
@@ -137,7 +136,7 @@ public class UpcomingInvoiceHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await AlignProviderTaxConcernsAsync(provider, subscription, parsedEvent.Id);
|
await AlignProviderTaxConcernsAsync(provider, subscription, customer, parsedEvent.Id);
|
||||||
|
|
||||||
await SendProviderUpcomingInvoiceEmailsAsync(new List<string> { provider.BillingEmail }, invoice, subscription, providerId.Value);
|
await SendProviderUpcomingInvoiceEmailsAsync(new List<string> { provider.BillingEmail }, invoice, subscription, providerId.Value);
|
||||||
}
|
}
|
||||||
@@ -199,13 +198,14 @@ public class UpcomingInvoiceHandler(
|
|||||||
private async Task AlignOrganizationTaxConcernsAsync(
|
private async Task AlignOrganizationTaxConcernsAsync(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
Subscription subscription,
|
Subscription subscription,
|
||||||
|
Customer customer,
|
||||||
string eventId)
|
string eventId)
|
||||||
{
|
{
|
||||||
var nonUSBusinessUse =
|
var nonUSBusinessUse =
|
||||||
organization.PlanType.GetProductTier() != ProductTierType.Families &&
|
organization.PlanType.GetProductTier() != ProductTierType.Families &&
|
||||||
subscription.Customer.Address.Country != Core.Constants.CountryAbbreviations.UnitedStates;
|
customer.Address.Country != Core.Constants.CountryAbbreviations.UnitedStates;
|
||||||
|
|
||||||
if (nonUSBusinessUse && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
|
if (nonUSBusinessUse && customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -246,10 +246,11 @@ public class UpcomingInvoiceHandler(
|
|||||||
private async Task AlignProviderTaxConcernsAsync(
|
private async Task AlignProviderTaxConcernsAsync(
|
||||||
Provider provider,
|
Provider provider,
|
||||||
Subscription subscription,
|
Subscription subscription,
|
||||||
|
Customer customer,
|
||||||
string eventId)
|
string eventId)
|
||||||
{
|
{
|
||||||
if (subscription.Customer.Address.Country != Core.Constants.CountryAbbreviations.UnitedStates &&
|
if (customer.Address.Country != Core.Constants.CountryAbbreviations.UnitedStates &&
|
||||||
subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
|
customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -57,9 +57,7 @@
|
|||||||
"billingSettings": {
|
"billingSettings": {
|
||||||
"jobsKey": "SECRET",
|
"jobsKey": "SECRET",
|
||||||
"stripeWebhookKey": "SECRET",
|
"stripeWebhookKey": "SECRET",
|
||||||
"stripeWebhookSecret": "SECRET",
|
"stripeWebhookSecret20250827Basil": "SECRET",
|
||||||
"stripeWebhookSecret20231016": "SECRET",
|
|
||||||
"stripeWebhookSecret20240620": "SECRET",
|
|
||||||
"bitPayWebhookKey": "SECRET",
|
"bitPayWebhookKey": "SECRET",
|
||||||
"appleWebhookKey": "SECRET",
|
"appleWebhookKey": "SECRET",
|
||||||
"payPal": {
|
"payPal": {
|
||||||
@@ -87,6 +85,6 @@
|
|||||||
"runSearch": "always",
|
"runSearch": "always",
|
||||||
"realTime": true
|
"realTime": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,11 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool SyncSeats { get; set; }
|
public bool SyncSeats { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If set to true, user accounts created within the organization are automatically confirmed without requiring additional verification steps.
|
||||||
|
/// </summary>
|
||||||
|
public bool UseAutomaticUserConfirmation { get; set; }
|
||||||
|
|
||||||
public void SetNewId()
|
public void SetNewId()
|
||||||
{
|
{
|
||||||
if (Id == default(Guid))
|
if (Id == default(Guid))
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public class OrganizationAbility
|
|||||||
UseRiskInsights = organization.UseRiskInsights;
|
UseRiskInsights = organization.UseRiskInsights;
|
||||||
UseOrganizationDomains = organization.UseOrganizationDomains;
|
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||||
|
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
@@ -49,4 +50,5 @@ public class OrganizationAbility
|
|||||||
public bool UseRiskInsights { get; set; }
|
public bool UseRiskInsights { get; set; }
|
||||||
public bool UseOrganizationDomains { get; set; }
|
public bool UseOrganizationDomains { get; set; }
|
||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
|
public bool UseAutomaticUserConfirmation { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,4 +66,5 @@ public class OrganizationUserOrganizationDetails
|
|||||||
public bool UseOrganizationDomains { get; set; }
|
public bool UseOrganizationDomains { get; set; }
|
||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
public bool? IsAdminInitiated { get; set; }
|
public bool? IsAdminInitiated { get; set; }
|
||||||
|
public bool UseAutomaticUserConfirmation { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,4 +51,5 @@ public class ProviderUserOrganizationDetails
|
|||||||
public bool UseOrganizationDomains { get; set; }
|
public bool UseOrganizationDomains { get; set; }
|
||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
public ProviderType ProviderType { get; set; }
|
public ProviderType ProviderType { get; set; }
|
||||||
|
public bool UseAutomaticUserConfirmation { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ public class UpdateOrganizationAuthRequestCommand : IUpdateOrganizationAuthReque
|
|||||||
AuthRequestExpiresAfter = _globalSettings.PasswordlessAuth.AdminRequestExpiration
|
AuthRequestExpiresAfter = _globalSettings.PasswordlessAuth.AdminRequestExpiration
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
processor.Process((Exception e) => _logger.LogError(e.Message));
|
processor.Process((Exception e) => _logger.LogError("Error processing organization auth request: {Message}", e.Message));
|
||||||
await processor.Save((IEnumerable<OrganizationAdminAuthRequest> authRequests) => _authRequestRepository.UpdateManyAsync(authRequests));
|
await processor.Save((IEnumerable<OrganizationAdminAuthRequest> authRequests) => _authRequestRepository.UpdateManyAsync(authRequests));
|
||||||
await processor.SendPushNotifications((ar) => _pushNotificationService.PushAuthRequestResponseAsync(ar));
|
await processor.SendPushNotifications((ar) => _pushNotificationService.PushAuthRequestResponseAsync(ar));
|
||||||
await processor.SendApprovalEmailsForProcessedRequests(SendApprovalEmail);
|
await processor.SendApprovalEmailsForProcessedRequests(SendApprovalEmail);
|
||||||
@@ -114,7 +114,7 @@ public class UpdateOrganizationAuthRequestCommand : IUpdateOrganizationAuthReque
|
|||||||
// This should be impossible
|
// This should be impossible
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
_logger.LogError($"User {authRequest.UserId} not found. Trusted device admin approval email not sent.");
|
_logger.LogError("User {UserId} not found. Trusted device admin approval email not sent.", authRequest.UserId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,25 +13,11 @@ public class VNextSavePolicyCommand(
|
|||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyRepository policyRepository,
|
||||||
IEnumerable<IEnforceDependentPoliciesEvent> policyValidationEventHandlers,
|
IEnumerable<IPolicyUpdateEvent> policyUpdateEventHandlers,
|
||||||
TimeProvider timeProvider,
|
TimeProvider timeProvider,
|
||||||
IPolicyEventHandlerFactory policyEventHandlerFactory)
|
IPolicyEventHandlerFactory policyEventHandlerFactory)
|
||||||
: IVNextSavePolicyCommand
|
: IVNextSavePolicyCommand
|
||||||
{
|
{
|
||||||
private readonly IReadOnlyDictionary<PolicyType, IEnforceDependentPoliciesEvent> _policyValidationEvents = MapToDictionary(policyValidationEventHandlers);
|
|
||||||
|
|
||||||
private static Dictionary<PolicyType, IEnforceDependentPoliciesEvent> MapToDictionary(IEnumerable<IEnforceDependentPoliciesEvent> policyValidationEventHandlers)
|
|
||||||
{
|
|
||||||
var policyValidationEventsDict = new Dictionary<PolicyType, IEnforceDependentPoliciesEvent>();
|
|
||||||
foreach (var policyValidationEvent in policyValidationEventHandlers)
|
|
||||||
{
|
|
||||||
if (!policyValidationEventsDict.TryAdd(policyValidationEvent.Type, policyValidationEvent))
|
|
||||||
{
|
|
||||||
throw new Exception($"Duplicate PolicyValidationEvent for {policyValidationEvent.Type} policy.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return policyValidationEventsDict;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Policy> SaveAsync(SavePolicyModel policyRequest)
|
public async Task<Policy> SaveAsync(SavePolicyModel policyRequest)
|
||||||
{
|
{
|
||||||
@@ -112,32 +98,26 @@ public class VNextSavePolicyCommand(
|
|||||||
Policy? currentPolicy,
|
Policy? currentPolicy,
|
||||||
Dictionary<PolicyType, Policy> savedPoliciesDict)
|
Dictionary<PolicyType, Policy> savedPoliciesDict)
|
||||||
{
|
{
|
||||||
var result = policyEventHandlerFactory.GetHandler<IEnforceDependentPoliciesEvent>(policyUpdateRequest.Type);
|
var isCurrentlyEnabled = currentPolicy?.Enabled == true;
|
||||||
|
var isBeingEnabled = policyUpdateRequest.Enabled && !isCurrentlyEnabled;
|
||||||
|
var isBeingDisabled = !policyUpdateRequest.Enabled && isCurrentlyEnabled;
|
||||||
|
|
||||||
result.Switch(
|
if (isBeingEnabled)
|
||||||
validator =>
|
{
|
||||||
{
|
ValidateEnablingRequirements(policyUpdateRequest.Type, savedPoliciesDict);
|
||||||
var isCurrentlyEnabled = currentPolicy?.Enabled == true;
|
}
|
||||||
|
else if (isBeingDisabled)
|
||||||
switch (policyUpdateRequest.Enabled)
|
{
|
||||||
{
|
ValidateDisablingRequirements(policyUpdateRequest.Type, savedPoliciesDict);
|
||||||
case true when !isCurrentlyEnabled:
|
}
|
||||||
ValidateEnablingRequirements(validator, savedPoliciesDict);
|
|
||||||
return;
|
|
||||||
case false when isCurrentlyEnabled:
|
|
||||||
ValidateDisablingRequirements(validator, policyUpdateRequest.Type, savedPoliciesDict);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => { });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ValidateDisablingRequirements(
|
private void ValidateDisablingRequirements(
|
||||||
IEnforceDependentPoliciesEvent validator,
|
|
||||||
PolicyType policyType,
|
PolicyType policyType,
|
||||||
Dictionary<PolicyType, Policy> savedPoliciesDict)
|
Dictionary<PolicyType, Policy> savedPoliciesDict)
|
||||||
{
|
{
|
||||||
var dependentPolicyTypes = _policyValidationEvents.Values
|
var dependentPolicyTypes = policyUpdateEventHandlers
|
||||||
|
.OfType<IEnforceDependentPoliciesEvent>()
|
||||||
.Where(otherValidator => otherValidator.RequiredPolicies.Contains(policyType))
|
.Where(otherValidator => otherValidator.RequiredPolicies.Contains(policyType))
|
||||||
.Select(otherValidator => otherValidator.Type)
|
.Select(otherValidator => otherValidator.Type)
|
||||||
.Where(otherPolicyType => savedPoliciesDict.TryGetValue(otherPolicyType, out var savedPolicy) &&
|
.Where(otherPolicyType => savedPoliciesDict.TryGetValue(otherPolicyType, out var savedPolicy) &&
|
||||||
@@ -147,24 +127,31 @@ public class VNextSavePolicyCommand(
|
|||||||
switch (dependentPolicyTypes)
|
switch (dependentPolicyTypes)
|
||||||
{
|
{
|
||||||
case { Count: 1 }:
|
case { Count: 1 }:
|
||||||
throw new BadRequestException($"Turn off the {dependentPolicyTypes.First().GetName()} policy because it requires the {validator.Type.GetName()} policy.");
|
throw new BadRequestException($"Turn off the {dependentPolicyTypes.First().GetName()} policy because it requires the {policyType.GetName()} policy.");
|
||||||
case { Count: > 1 }:
|
case { Count: > 1 }:
|
||||||
throw new BadRequestException($"Turn off all of the policies that require the {validator.Type.GetName()} policy.");
|
throw new BadRequestException($"Turn off all of the policies that require the {policyType.GetName()} policy.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ValidateEnablingRequirements(
|
private void ValidateEnablingRequirements(
|
||||||
IEnforceDependentPoliciesEvent validator,
|
PolicyType policyType,
|
||||||
Dictionary<PolicyType, Policy> savedPoliciesDict)
|
Dictionary<PolicyType, Policy> savedPoliciesDict)
|
||||||
{
|
{
|
||||||
var missingRequiredPolicyTypes = validator.RequiredPolicies
|
var result = policyEventHandlerFactory.GetHandler<IEnforceDependentPoliciesEvent>(policyType);
|
||||||
.Where(requiredPolicyType => savedPoliciesDict.GetValueOrDefault(requiredPolicyType) is not { Enabled: true })
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (missingRequiredPolicyTypes.Count != 0)
|
result.Switch(
|
||||||
{
|
validator =>
|
||||||
throw new BadRequestException($"Turn on the {missingRequiredPolicyTypes.First().GetName()} policy because it is required for the {validator.Type.GetName()} policy.");
|
{
|
||||||
}
|
var missingRequiredPolicyTypes = validator.RequiredPolicies
|
||||||
|
.Where(requiredPolicyType => savedPoliciesDict.GetValueOrDefault(requiredPolicyType) is not { Enabled: true })
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (missingRequiredPolicyTypes.Count != 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Turn on the {missingRequiredPolicyTypes.First().GetName()} policy because it is required for the {policyType.GetName()} policy.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => { /* Policy has no required dependencies */ });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ExecutePreUpsertSideEffectAsync(
|
private async Task ExecutePreUpsertSideEffectAsync(
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ public static class PolicyServiceCollectionExtensions
|
|||||||
services.AddPolicyValidators();
|
services.AddPolicyValidators();
|
||||||
services.AddPolicyRequirements();
|
services.AddPolicyRequirements();
|
||||||
services.AddPolicySideEffects();
|
services.AddPolicySideEffects();
|
||||||
|
services.AddPolicyUpdateEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Obsolete("Use AddPolicyUpdateEvents instead.")]
|
||||||
private static void AddPolicyValidators(this IServiceCollection services)
|
private static void AddPolicyValidators(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<IPolicyValidator, TwoFactorAuthenticationPolicyValidator>();
|
services.AddScoped<IPolicyValidator, TwoFactorAuthenticationPolicyValidator>();
|
||||||
@@ -34,11 +36,23 @@ public static class PolicyServiceCollectionExtensions
|
|||||||
services.AddScoped<IPolicyValidator, FreeFamiliesForEnterprisePolicyValidator>();
|
services.AddScoped<IPolicyValidator, FreeFamiliesForEnterprisePolicyValidator>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Obsolete("Use AddPolicyUpdateEvents instead.")]
|
||||||
private static void AddPolicySideEffects(this IServiceCollection services)
|
private static void AddPolicySideEffects(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<IPostSavePolicySideEffect, OrganizationDataOwnershipPolicyValidator>();
|
services.AddScoped<IPostSavePolicySideEffect, OrganizationDataOwnershipPolicyValidator>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AddPolicyUpdateEvents(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<IPolicyUpdateEvent, RequireSsoPolicyValidator>();
|
||||||
|
services.AddScoped<IPolicyUpdateEvent, TwoFactorAuthenticationPolicyValidator>();
|
||||||
|
services.AddScoped<IPolicyUpdateEvent, SingleOrgPolicyValidator>();
|
||||||
|
services.AddScoped<IPolicyUpdateEvent, ResetPasswordPolicyValidator>();
|
||||||
|
services.AddScoped<IPolicyUpdateEvent, MaximumVaultTimeoutPolicyValidator>();
|
||||||
|
services.AddScoped<IPolicyUpdateEvent, FreeFamiliesForEnterprisePolicyValidator>();
|
||||||
|
services.AddScoped<IPolicyUpdateEvent, OrganizationDataOwnershipPolicyValidator>();
|
||||||
|
}
|
||||||
|
|
||||||
private static void AddPolicyRequirements(this IServiceCollection services)
|
private static void AddPolicyRequirements(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, DisableSendPolicyRequirementFactory>();
|
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, DisableSendPolicyRequirementFactory>();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
|
||||||
@@ -12,11 +13,16 @@ public class FreeFamiliesForEnterprisePolicyValidator(
|
|||||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
IOrganizationRepository organizationRepository)
|
IOrganizationRepository organizationRepository)
|
||||||
: IPolicyValidator
|
: IPolicyValidator, IOnPolicyPreUpdateEvent
|
||||||
{
|
{
|
||||||
public PolicyType Type => PolicyType.FreeFamiliesSponsorshipPolicy;
|
public PolicyType Type => PolicyType.FreeFamiliesSponsorshipPolicy;
|
||||||
public IEnumerable<PolicyType> RequiredPolicies => [];
|
public IEnumerable<PolicyType> RequiredPolicies => [];
|
||||||
|
|
||||||
|
public async Task ExecutePreUpsertSideEffectAsync(SavePolicyModel policyRequest, Policy? currentPolicy)
|
||||||
|
{
|
||||||
|
await OnSaveSideEffectsAsync(policyRequest.PolicyUpdate, currentPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||||
{
|
{
|
||||||
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||||
|
|
||||||
public class MaximumVaultTimeoutPolicyValidator : IPolicyValidator
|
public class MaximumVaultTimeoutPolicyValidator : IPolicyValidator, IEnforceDependentPoliciesEvent
|
||||||
{
|
{
|
||||||
public PolicyType Type => PolicyType.MaximumVaultTimeout;
|
public PolicyType Type => PolicyType.MaximumVaultTimeout;
|
||||||
public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];
|
public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];
|
||||||
|
|||||||
@@ -1,24 +1,32 @@
|
|||||||
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Please do not extend or expand this validator. We're currently in the process of refactoring our policy validator pattern.
|
|
||||||
/// This is a stop-gap solution for post-policy-save side effects, but it is not the long-term solution.
|
|
||||||
/// </summary>
|
|
||||||
public class OrganizationDataOwnershipPolicyValidator(
|
public class OrganizationDataOwnershipPolicyValidator(
|
||||||
IPolicyRepository policyRepository,
|
IPolicyRepository policyRepository,
|
||||||
ICollectionRepository collectionRepository,
|
ICollectionRepository collectionRepository,
|
||||||
IEnumerable<IPolicyRequirementFactory<IPolicyRequirement>> factories,
|
IEnumerable<IPolicyRequirementFactory<IPolicyRequirement>> factories,
|
||||||
IFeatureService featureService)
|
IFeatureService featureService)
|
||||||
: OrganizationPolicyValidator(policyRepository, factories), IPostSavePolicySideEffect
|
: OrganizationPolicyValidator(policyRepository, factories), IPostSavePolicySideEffect, IOnPolicyPostUpdateEvent
|
||||||
{
|
{
|
||||||
|
public PolicyType Type => PolicyType.OrganizationDataOwnership;
|
||||||
|
|
||||||
|
public async Task ExecutePostUpsertSideEffectAsync(
|
||||||
|
SavePolicyModel policyRequest,
|
||||||
|
Policy postUpsertedPolicyState,
|
||||||
|
Policy? previousPolicyState)
|
||||||
|
{
|
||||||
|
await ExecuteSideEffectsAsync(policyRequest, postUpsertedPolicyState, previousPolicyState);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ExecuteSideEffectsAsync(
|
public async Task ExecuteSideEffectsAsync(
|
||||||
SavePolicyModel policyRequest,
|
SavePolicyModel policyRequest,
|
||||||
Policy postUpdatedPolicy,
|
Policy postUpdatedPolicy,
|
||||||
@@ -68,5 +76,4 @@ public class OrganizationDataOwnershipPolicyValidator(
|
|||||||
userOrgIds,
|
userOrgIds,
|
||||||
defaultCollectionName);
|
defaultCollectionName);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||||
|
|
||||||
public class RequireSsoPolicyValidator : IPolicyValidator
|
public class RequireSsoPolicyValidator : IPolicyValidator, IPolicyValidationEvent, IEnforceDependentPoliciesEvent
|
||||||
{
|
{
|
||||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||||
|
|
||||||
@@ -20,6 +21,11 @@ public class RequireSsoPolicyValidator : IPolicyValidator
|
|||||||
public PolicyType Type => PolicyType.RequireSso;
|
public PolicyType Type => PolicyType.RequireSso;
|
||||||
public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];
|
public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];
|
||||||
|
|
||||||
|
public async Task<string> ValidateAsync(SavePolicyModel policyRequest, Policy? currentPolicy)
|
||||||
|
{
|
||||||
|
return await ValidateAsync(policyRequest.PolicyUpdate, currentPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||||
{
|
{
|
||||||
if (policyUpdate is not { Enabled: true })
|
if (policyUpdate is not { Enabled: true })
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ using Bit.Core.AdminConsole.Entities;
|
|||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||||
|
|
||||||
public class ResetPasswordPolicyValidator : IPolicyValidator
|
public class ResetPasswordPolicyValidator : IPolicyValidator, IPolicyValidationEvent, IEnforceDependentPoliciesEvent
|
||||||
{
|
{
|
||||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||||
public PolicyType Type => PolicyType.ResetPassword;
|
public PolicyType Type => PolicyType.ResetPassword;
|
||||||
@@ -20,6 +21,11 @@ public class ResetPasswordPolicyValidator : IPolicyValidator
|
|||||||
_ssoConfigRepository = ssoConfigRepository;
|
_ssoConfigRepository = ssoConfigRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string> ValidateAsync(SavePolicyModel policyRequest, Policy? currentPolicy)
|
||||||
|
{
|
||||||
|
return await ValidateAsync(policyRequest.PolicyUpdate, currentPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||||
{
|
{
|
||||||
if (policyUpdate is not { Enabled: true } ||
|
if (policyUpdate is not { Enabled: true } ||
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@@ -17,7 +18,7 @@ using Bit.Core.Services;
|
|||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||||
|
|
||||||
public class SingleOrgPolicyValidator : IPolicyValidator
|
public class SingleOrgPolicyValidator : IPolicyValidator, IPolicyValidationEvent, IOnPolicyPreUpdateEvent
|
||||||
{
|
{
|
||||||
public PolicyType Type => PolicyType.SingleOrg;
|
public PolicyType Type => PolicyType.SingleOrg;
|
||||||
private const string OrganizationNotFoundErrorMessage = "Organization not found.";
|
private const string OrganizationNotFoundErrorMessage = "Organization not found.";
|
||||||
@@ -57,6 +58,16 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
|||||||
|
|
||||||
public IEnumerable<PolicyType> RequiredPolicies => [];
|
public IEnumerable<PolicyType> RequiredPolicies => [];
|
||||||
|
|
||||||
|
public async Task<string> ValidateAsync(SavePolicyModel policyRequest, Policy? currentPolicy)
|
||||||
|
{
|
||||||
|
return await ValidateAsync(policyRequest.PolicyUpdate, currentPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExecutePreUpsertSideEffectAsync(SavePolicyModel policyRequest, Policy? currentPolicy)
|
||||||
|
{
|
||||||
|
await OnSaveSideEffectsAsync(policyRequest.PolicyUpdate, currentPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||||
{
|
{
|
||||||
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Models.Data;
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@@ -16,7 +17,7 @@ using Bit.Core.Services;
|
|||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||||
|
|
||||||
public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator, IOnPolicyPreUpdateEvent
|
||||||
{
|
{
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
@@ -46,6 +47,11 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
|||||||
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ExecutePreUpsertSideEffectAsync(SavePolicyModel policyRequest, Policy? currentPolicy)
|
||||||
|
{
|
||||||
|
await OnSaveSideEffectsAsync(policyRequest.PolicyUpdate, currentPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||||
{
|
{
|
||||||
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
||||||
|
|||||||
@@ -87,4 +87,13 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
|||||||
Task<IEnumerable<OrganizationUserUserDetails>> GetManyDetailsByRoleAsync(Guid organizationId, OrganizationUserType role);
|
Task<IEnumerable<OrganizationUserUserDetails>> GetManyDetailsByRoleAsync(Guid organizationId, OrganizationUserType role);
|
||||||
|
|
||||||
Task CreateManyAsync(IEnumerable<CreateOrganizationUser> organizationUserCollection);
|
Task CreateManyAsync(IEnumerable<CreateOrganizationUser> organizationUserCollection);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// It will only confirm if the user is in the `Accepted` state.
|
||||||
|
///
|
||||||
|
/// This is an idempotent operation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationUser">Accepted OrganizationUser to confirm</param>
|
||||||
|
/// <returns>True, if the user was updated. False, if not performed.</returns>
|
||||||
|
Task<bool> ConfirmOrganizationUserAsync(OrganizationUser organizationUser);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ public static class OrganizationFactory
|
|||||||
claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseOrganizationDomains),
|
claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseOrganizationDomains),
|
||||||
UseAdminSponsoredFamilies =
|
UseAdminSponsoredFamilies =
|
||||||
claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseAdminSponsoredFamilies),
|
claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseAdminSponsoredFamilies),
|
||||||
|
UseAutomaticUserConfirmation = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseAutomaticUserConfirmation),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static Organization Create(
|
public static Organization Create(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Bit.Core.KeyManagement.Models.Response;
|
using Bit.Core.KeyManagement.Models.Api.Response;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
|
|
||||||
namespace Bit.Core.Auth.Models.Api.Response;
|
namespace Bit.Core.Auth.Models.Api.Response;
|
||||||
|
|||||||
@@ -64,10 +64,12 @@ public static class InvoiceExtensions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var tax = invoice.TotalTaxes?.Sum(invoiceTotalTax => invoiceTotalTax.Amount) ?? 0;
|
||||||
|
|
||||||
// Add fallback tax from invoice-level tax if present and not already included
|
// Add fallback tax from invoice-level tax if present and not already included
|
||||||
if (invoice.Tax.HasValue && invoice.Tax.Value > 0)
|
if (tax > 0)
|
||||||
{
|
{
|
||||||
var taxAmount = invoice.Tax.Value / 100m;
|
var taxAmount = tax / 100m;
|
||||||
items.Add($"1 × Tax (at ${taxAmount:F2} / month)");
|
items.Add($"1 × Tax (at ${taxAmount:F2} / month)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddPaymentOperations();
|
services.AddPaymentOperations();
|
||||||
services.AddOrganizationLicenseCommandsQueries();
|
services.AddOrganizationLicenseCommandsQueries();
|
||||||
services.AddPremiumCommands();
|
services.AddPremiumCommands();
|
||||||
|
services.AddTransient<IGetOrganizationMetadataQuery, GetOrganizationMetadataQuery>();
|
||||||
services.AddTransient<IGetOrganizationWarningsQuery, GetOrganizationWarningsQuery>();
|
services.AddTransient<IGetOrganizationWarningsQuery, GetOrganizationWarningsQuery>();
|
||||||
services.AddTransient<IRestartSubscriptionCommand, RestartSubscriptionCommand>();
|
services.AddTransient<IRestartSubscriptionCommand, RestartSubscriptionCommand>();
|
||||||
services.AddTransient<IPreviewOrganizationTaxCommand, PreviewOrganizationTaxCommand>();
|
services.AddTransient<IPreviewOrganizationTaxCommand, PreviewOrganizationTaxCommand>();
|
||||||
|
|||||||
25
src/Core/Billing/Extensions/SubscriptionExtensions.cs
Normal file
25
src/Core/Billing/Extensions/SubscriptionExtensions.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using Stripe;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Extensions;
|
||||||
|
|
||||||
|
public static class SubscriptionExtensions
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* For the time being, this is the simplest migration approach from v45 to v48 as
|
||||||
|
* we do not support multi-cadence subscriptions. Each subscription item should be on the
|
||||||
|
* same billing cycle. If this changes, we'll need a significantly more robust approach.
|
||||||
|
*
|
||||||
|
* Because we can't guarantee a subscription will have items, this has to be nullable.
|
||||||
|
*/
|
||||||
|
public static (DateTime? Start, DateTime? End)? GetCurrentPeriod(this Subscription subscription)
|
||||||
|
{
|
||||||
|
var item = subscription.Items?.FirstOrDefault();
|
||||||
|
return item is null ? null : (item.CurrentPeriodStart, item.CurrentPeriodEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DateTime? GetCurrentPeriodStart(this Subscription subscription) =>
|
||||||
|
subscription.Items?.FirstOrDefault()?.CurrentPeriodStart;
|
||||||
|
|
||||||
|
public static DateTime? GetCurrentPeriodEnd(this Subscription subscription) =>
|
||||||
|
subscription.Items?.FirstOrDefault()?.CurrentPeriodEnd;
|
||||||
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
using Stripe;
|
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Extensions;
|
|
||||||
|
|
||||||
public static class UpcomingInvoiceOptionsExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Attempts to enable automatic tax for given upcoming invoice options.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options"></param>
|
|
||||||
/// <param name="customer">The existing customer to which the upcoming invoice belongs.</param>
|
|
||||||
/// <param name="subscription">The existing subscription to which the upcoming invoice belongs.</param>
|
|
||||||
/// <returns>Returns true when successful, false when conditions are not met.</returns>
|
|
||||||
public static bool EnableAutomaticTax(
|
|
||||||
this UpcomingInvoiceOptions options,
|
|
||||||
Customer customer,
|
|
||||||
Subscription subscription)
|
|
||||||
{
|
|
||||||
if (subscription != null && subscription.AutomaticTax.Enabled)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We might only need to check the automatic tax status.
|
|
||||||
if (!customer.HasRecognizedTaxLocation() && string.IsNullOrWhiteSpace(customer.Address?.Country))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true };
|
|
||||||
options.SubscriptionDefaultTaxRates = [];
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -43,6 +43,7 @@ public static class OrganizationLicenseConstants
|
|||||||
public const string Trial = nameof(Trial);
|
public const string Trial = nameof(Trial);
|
||||||
public const string UseAdminSponsoredFamilies = nameof(UseAdminSponsoredFamilies);
|
public const string UseAdminSponsoredFamilies = nameof(UseAdminSponsoredFamilies);
|
||||||
public const string UseOrganizationDomains = nameof(UseOrganizationDomains);
|
public const string UseOrganizationDomains = nameof(UseOrganizationDomains);
|
||||||
|
public const string UseAutomaticUserConfirmation = nameof(UseAutomaticUserConfirmation);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class UserLicenseConstants
|
public static class UserLicenseConstants
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organizati
|
|||||||
new(nameof(OrganizationLicenseConstants.Trial), trial.ToString()),
|
new(nameof(OrganizationLicenseConstants.Trial), trial.ToString()),
|
||||||
new(nameof(OrganizationLicenseConstants.UseAdminSponsoredFamilies), entity.UseAdminSponsoredFamilies.ToString()),
|
new(nameof(OrganizationLicenseConstants.UseAdminSponsoredFamilies), entity.UseAdminSponsoredFamilies.ToString()),
|
||||||
new(nameof(OrganizationLicenseConstants.UseOrganizationDomains), entity.UseOrganizationDomains.ToString()),
|
new(nameof(OrganizationLicenseConstants.UseOrganizationDomains), entity.UseOrganizationDomains.ToString()),
|
||||||
|
new(nameof(OrganizationLicenseConstants.UseAutomaticUserConfirmation), entity.UseAutomaticUserConfirmation.ToString()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (entity.Name is not null)
|
if (entity.Name is not null)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// FIXME: Update this file to be null safe and then delete the line below
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
@@ -46,7 +47,7 @@ public class BillingHistoryInfo
|
|||||||
Url = inv.HostedInvoiceUrl;
|
Url = inv.HostedInvoiceUrl;
|
||||||
PdfUrl = inv.InvoicePdf;
|
PdfUrl = inv.InvoicePdf;
|
||||||
Number = inv.Number;
|
Number = inv.Number;
|
||||||
Paid = inv.Paid;
|
Paid = inv.Status == StripeConstants.InvoiceStatus.Paid;
|
||||||
Amount = inv.Total / 100M;
|
Amount = inv.Total / 100M;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ public abstract record Plan
|
|||||||
public SecretsManagerPlanFeatures SecretsManager { get; protected init; }
|
public SecretsManagerPlanFeatures SecretsManager { get; protected init; }
|
||||||
public bool SupportsSecretsManager => SecretsManager != null;
|
public bool SupportsSecretsManager => SecretsManager != null;
|
||||||
|
|
||||||
|
public bool AutomaticUserConfirmation { get; init; }
|
||||||
|
|
||||||
public bool HasNonSeatBasedPasswordManagerPlan() =>
|
public bool HasNonSeatBasedPasswordManagerPlan() =>
|
||||||
PasswordManager is { StripePlanId: not null and not "", StripeSeatPlanId: null or "" };
|
PasswordManager is { StripePlanId: not null and not "", StripeSeatPlanId: null or "" };
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,13 @@ public class PreviewOrganizationTaxCommand(
|
|||||||
Quantity = purchase.SecretsManager.Seats
|
Quantity = purchase.SecretsManager.Seats
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
options.Coupon = CouponIDs.SecretsManagerStandalone;
|
options.Discounts =
|
||||||
|
[
|
||||||
|
new InvoiceDiscountOptions
|
||||||
|
{
|
||||||
|
Coupon = CouponIDs.SecretsManagerStandalone
|
||||||
|
}
|
||||||
|
];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -180,7 +186,10 @@ public class PreviewOrganizationTaxCommand(
|
|||||||
|
|
||||||
if (subscription.Customer.Discount != null)
|
if (subscription.Customer.Discount != null)
|
||||||
{
|
{
|
||||||
options.Coupon = subscription.Customer.Discount.Coupon.Id;
|
options.Discounts =
|
||||||
|
[
|
||||||
|
new InvoiceDiscountOptions { Coupon = subscription.Customer.Discount.Coupon.Id }
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentPlan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
var currentPlan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||||
@@ -277,7 +286,10 @@ public class PreviewOrganizationTaxCommand(
|
|||||||
|
|
||||||
if (subscription.Customer.Discount != null)
|
if (subscription.Customer.Discount != null)
|
||||||
{
|
{
|
||||||
options.Coupon = subscription.Customer.Discount.Coupon.Id;
|
options.Discounts =
|
||||||
|
[
|
||||||
|
new InvoiceDiscountOptions { Coupon = subscription.Customer.Discount.Coupon.Id }
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentPlan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
var currentPlan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||||
@@ -329,7 +341,7 @@ public class PreviewOrganizationTaxCommand(
|
|||||||
});
|
});
|
||||||
|
|
||||||
private static (decimal, decimal) GetAmounts(Invoice invoice) => (
|
private static (decimal, decimal) GetAmounts(Invoice invoice) => (
|
||||||
Convert.ToDecimal(invoice.Tax) / 100,
|
Convert.ToDecimal(invoice.TotalTaxes.Sum(invoiceTotalTax => invoiceTotalTax.Amount)) / 100,
|
||||||
Convert.ToDecimal(invoice.Total) / 100);
|
Convert.ToDecimal(invoice.Total) / 100);
|
||||||
|
|
||||||
private static InvoiceCreatePreviewOptions GetBaseOptions(
|
private static InvoiceCreatePreviewOptions GetBaseOptions(
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ public class OrganizationLicense : ILicense
|
|||||||
public LicenseType? LicenseType { get; set; }
|
public LicenseType? LicenseType { get; set; }
|
||||||
public bool UseOrganizationDomains { get; set; }
|
public bool UseOrganizationDomains { get; set; }
|
||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
|
public bool UseAutomaticUserConfirmation { get; set; }
|
||||||
public string Hash { get; set; }
|
public string Hash { get; set; }
|
||||||
public string Signature { get; set; }
|
public string Signature { get; set; }
|
||||||
public string Token { get; set; }
|
public string Token { get; set; }
|
||||||
@@ -226,7 +227,8 @@ public class OrganizationLicense : ILicense
|
|||||||
// any new fields added need to be added here so that they're ignored
|
// any new fields added need to be added here so that they're ignored
|
||||||
!p.Name.Equals(nameof(UseRiskInsights)) &&
|
!p.Name.Equals(nameof(UseRiskInsights)) &&
|
||||||
!p.Name.Equals(nameof(UseAdminSponsoredFamilies)) &&
|
!p.Name.Equals(nameof(UseAdminSponsoredFamilies)) &&
|
||||||
!p.Name.Equals(nameof(UseOrganizationDomains)))
|
!p.Name.Equals(nameof(UseOrganizationDomains)) &&
|
||||||
|
!p.Name.Equals(nameof(UseAutomaticUserConfirmation)))
|
||||||
.OrderBy(p => p.Name)
|
.OrderBy(p => p.Name)
|
||||||
.Select(p => $"{p.Name}:{Core.Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}")
|
.Select(p => $"{p.Name}:{Core.Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}")
|
||||||
.Aggregate((c, n) => $"{c}|{n}");
|
.Aggregate((c, n) => $"{c}|{n}");
|
||||||
@@ -421,6 +423,7 @@ public class OrganizationLicense : ILicense
|
|||||||
var smServiceAccounts = claimsPrincipal.GetValue<int?>(nameof(SmServiceAccounts));
|
var smServiceAccounts = claimsPrincipal.GetValue<int?>(nameof(SmServiceAccounts));
|
||||||
var useAdminSponsoredFamilies = claimsPrincipal.GetValue<bool>(nameof(UseAdminSponsoredFamilies));
|
var useAdminSponsoredFamilies = claimsPrincipal.GetValue<bool>(nameof(UseAdminSponsoredFamilies));
|
||||||
var useOrganizationDomains = claimsPrincipal.GetValue<bool>(nameof(UseOrganizationDomains));
|
var useOrganizationDomains = claimsPrincipal.GetValue<bool>(nameof(UseOrganizationDomains));
|
||||||
|
var useAutomaticUserConfirmation = claimsPrincipal.GetValue<bool>(nameof(UseAutomaticUserConfirmation));
|
||||||
|
|
||||||
return issued <= DateTime.UtcNow &&
|
return issued <= DateTime.UtcNow &&
|
||||||
expires >= DateTime.UtcNow &&
|
expires >= DateTime.UtcNow &&
|
||||||
@@ -450,7 +453,8 @@ public class OrganizationLicense : ILicense
|
|||||||
smSeats == organization.SmSeats &&
|
smSeats == organization.SmSeats &&
|
||||||
smServiceAccounts == organization.SmServiceAccounts &&
|
smServiceAccounts == organization.SmServiceAccounts &&
|
||||||
useAdminSponsoredFamilies == organization.UseAdminSponsoredFamilies &&
|
useAdminSponsoredFamilies == organization.UseAdminSponsoredFamilies &&
|
||||||
useOrganizationDomains == organization.UseOrganizationDomains;
|
useOrganizationDomains == organization.UseOrganizationDomains &&
|
||||||
|
useAutomaticUserConfirmation == organization.UseAutomaticUserConfirmation;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,10 @@
|
|||||||
namespace Bit.Core.Billing.Organizations.Models;
|
namespace Bit.Core.Billing.Organizations.Models;
|
||||||
|
|
||||||
public record OrganizationMetadata(
|
public record OrganizationMetadata(
|
||||||
bool IsEligibleForSelfHost,
|
|
||||||
bool IsManaged,
|
|
||||||
bool IsOnSecretsManagerStandalone,
|
bool IsOnSecretsManagerStandalone,
|
||||||
bool IsSubscriptionUnpaid,
|
|
||||||
bool HasSubscription,
|
|
||||||
bool HasOpenInvoice,
|
|
||||||
bool IsSubscriptionCanceled,
|
|
||||||
DateTime? InvoiceDueDate,
|
|
||||||
DateTime? InvoiceCreatedDate,
|
|
||||||
DateTime? SubPeriodEndDate,
|
|
||||||
int OrganizationOccupiedSeats)
|
int OrganizationOccupiedSeats)
|
||||||
{
|
{
|
||||||
public static OrganizationMetadata Default => new OrganizationMetadata(
|
public static OrganizationMetadata Default => new OrganizationMetadata(
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
0);
|
0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace Bit.Core.Billing.Organizations.Models;
|
|||||||
|
|
||||||
public class OrganizationSale
|
public class OrganizationSale
|
||||||
{
|
{
|
||||||
private OrganizationSale() { }
|
internal OrganizationSale() { }
|
||||||
|
|
||||||
public void Deconstruct(
|
public void Deconstruct(
|
||||||
out Organization organization,
|
out Organization organization,
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Organizations.Models;
|
||||||
|
using Bit.Core.Billing.Pricing;
|
||||||
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Organizations.Queries;
|
||||||
|
|
||||||
|
public interface IGetOrganizationMetadataQuery
|
||||||
|
{
|
||||||
|
Task<OrganizationMetadata?> Run(Organization organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GetOrganizationMetadataQuery(
|
||||||
|
IGlobalSettings globalSettings,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IPricingClient pricingClient,
|
||||||
|
ISubscriberService subscriberService) : IGetOrganizationMetadataQuery
|
||||||
|
{
|
||||||
|
public async Task<OrganizationMetadata?> Run(Organization organization)
|
||||||
|
{
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (globalSettings.SelfHosted)
|
||||||
|
{
|
||||||
|
return OrganizationMetadata.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgOccupiedSeats = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
||||||
|
{
|
||||||
|
return OrganizationMetadata.Default with
|
||||||
|
{
|
||||||
|
OrganizationOccupiedSeats = orgOccupiedSeats.Total
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var customer = await subscriberService.GetCustomer(organization,
|
||||||
|
new CustomerGetOptions { Expand = ["discount.coupon.applies_to"] });
|
||||||
|
|
||||||
|
var subscription = await subscriberService.GetSubscription(organization);
|
||||||
|
|
||||||
|
if (customer == null || subscription == null)
|
||||||
|
{
|
||||||
|
return OrganizationMetadata.Default with
|
||||||
|
{
|
||||||
|
OrganizationOccupiedSeats = orgOccupiedSeats.Total
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var isOnSecretsManagerStandalone = await IsOnSecretsManagerStandalone(organization, customer, subscription);
|
||||||
|
|
||||||
|
return new OrganizationMetadata(
|
||||||
|
isOnSecretsManagerStandalone,
|
||||||
|
orgOccupiedSeats.Total);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> IsOnSecretsManagerStandalone(
|
||||||
|
Organization organization,
|
||||||
|
Customer? customer,
|
||||||
|
Subscription? subscription)
|
||||||
|
{
|
||||||
|
if (customer == null || subscription == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||||
|
|
||||||
|
if (!plan.SupportsSecretsManager)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasCoupon = customer.Discount?.Coupon?.Id == StripeConstants.CouponIDs.SecretsManagerStandalone;
|
||||||
|
|
||||||
|
if (!hasCoupon)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscriptionProductIds = subscription.Items.Data.Select(item => item.Plan.ProductId);
|
||||||
|
|
||||||
|
var couponAppliesTo = customer.Discount?.Coupon?.AppliesTo?.Products;
|
||||||
|
|
||||||
|
return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -162,17 +162,23 @@ public class GetOrganizationWarningsQuery(
|
|||||||
if (subscription is
|
if (subscription is
|
||||||
{
|
{
|
||||||
Status: SubscriptionStatus.Trialing or SubscriptionStatus.Active,
|
Status: SubscriptionStatus.Trialing or SubscriptionStatus.Active,
|
||||||
LatestInvoice: null or { Status: InvoiceStatus.Paid }
|
LatestInvoice: null or { Status: InvoiceStatus.Paid },
|
||||||
} && (subscription.CurrentPeriodEnd - now).TotalDays <= 14)
|
Items.Data.Count: > 0
|
||||||
|
})
|
||||||
{
|
{
|
||||||
return new ResellerRenewalWarning
|
var currentPeriodEnd = subscription.GetCurrentPeriodEnd();
|
||||||
|
|
||||||
|
if (currentPeriodEnd != null && (currentPeriodEnd.Value - now).TotalDays <= 14)
|
||||||
{
|
{
|
||||||
Type = "upcoming",
|
return new ResellerRenewalWarning
|
||||||
Upcoming = new ResellerRenewalWarning.UpcomingRenewal
|
|
||||||
{
|
{
|
||||||
RenewalDate = subscription.CurrentPeriodEnd
|
Type = "upcoming",
|
||||||
}
|
Upcoming = new ResellerRenewalWarning.UpcomingRenewal
|
||||||
};
|
{
|
||||||
|
RenewalDate = currentPeriodEnd.Value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subscription is
|
if (subscription is
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user