mirror of
https://github.com/bitwarden/server.git
synced 2024-11-25 12:45:18 +01:00
Merge branch 'main' into billing/PM-11516/license-refactor
This commit is contained in:
commit
ee8763fbce
35
.github/workflows/repository-management.yml
vendored
35
.github/workflows/repository-management.yml
vendored
@ -28,7 +28,6 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
branch: ${{ steps.set-branch.outputs.branch }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
steps:
|
||||
- name: Set branch
|
||||
id: set-branch
|
||||
@ -45,13 +44,6 @@ jobs:
|
||||
|
||||
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||
|
||||
|
||||
cut_branch:
|
||||
name: Cut branch
|
||||
@ -59,11 +51,18 @@ jobs:
|
||||
needs: setup
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||
|
||||
- name: Check out target ref
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.target_ref }}
|
||||
token: ${{ needs.setup.outputs.token }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Check if ${{ needs.setup.outputs.branch }} branch exists
|
||||
env:
|
||||
@ -98,11 +97,18 @@ jobs:
|
||||
with:
|
||||
version: ${{ inputs.version_number_override }}
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||
|
||||
- name: Check out branch
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ needs.setup.outputs.token }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
@ -190,11 +196,18 @@ jobs:
|
||||
- bump_version
|
||||
- setup
|
||||
steps:
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||
|
||||
- name: Check out main branch
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ needs.setup.outputs.token }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
@ -10,7 +9,6 @@ using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Commercial.Core.AdminConsole.Providers;
|
||||
|
||||
@ -21,35 +19,28 @@ public class CreateProviderCommand : ICreateProviderCommand
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IProviderPlanRepository _providerPlanRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public CreateProviderCommand(
|
||||
IProviderRepository providerRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IProviderService providerService,
|
||||
IUserRepository userRepository,
|
||||
IProviderPlanRepository providerPlanRepository,
|
||||
IFeatureService featureService)
|
||||
IProviderPlanRepository providerPlanRepository)
|
||||
{
|
||||
_providerRepository = providerRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_providerService = providerService;
|
||||
_userRepository = userRepository;
|
||||
_providerPlanRepository = providerPlanRepository;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
public async Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats)
|
||||
{
|
||||
var providerId = await CreateProviderAsync(provider, ownerEmail);
|
||||
|
||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
|
||||
if (isConsolidatedBillingEnabled)
|
||||
{
|
||||
await CreateProviderPlanAsync(providerId, PlanType.TeamsMonthly, teamsMinimumSeats);
|
||||
await CreateProviderPlanAsync(providerId, PlanType.EnterpriseMonthly, enterpriseMinimumSeats);
|
||||
}
|
||||
await Task.WhenAll(
|
||||
CreateProviderPlanAsync(providerId, PlanType.TeamsMonthly, teamsMinimumSeats),
|
||||
CreateProviderPlanAsync(providerId, PlanType.EnterpriseMonthly, enterpriseMinimumSeats));
|
||||
}
|
||||
|
||||
public async Task CreateResellerAsync(Provider provider)
|
||||
@ -61,12 +52,7 @@ public class CreateProviderCommand : ICreateProviderCommand
|
||||
{
|
||||
var providerId = await CreateProviderAsync(provider, ownerEmail);
|
||||
|
||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
|
||||
if (isConsolidatedBillingEnabled)
|
||||
{
|
||||
await CreateProviderPlanAsync(providerId, plan, minimumSeats);
|
||||
}
|
||||
await CreateProviderPlanAsync(providerId, plan, minimumSeats);
|
||||
}
|
||||
|
||||
private async Task<Guid> CreateProviderAsync(Provider provider, string ownerEmail)
|
||||
@ -77,12 +63,7 @@ public class CreateProviderCommand : ICreateProviderCommand
|
||||
throw new BadRequestException("Invalid owner. Owner must be an existing Bitwarden user.");
|
||||
}
|
||||
|
||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
|
||||
if (isConsolidatedBillingEnabled)
|
||||
{
|
||||
provider.Gateway = GatewayType.Stripe;
|
||||
}
|
||||
provider.Gateway = GatewayType.Stripe;
|
||||
|
||||
await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Pending);
|
||||
|
||||
|
@ -1,7 +1,5 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
@ -102,11 +100,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
Provider provider,
|
||||
IEnumerable<string> organizationOwnerEmails)
|
||||
{
|
||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
|
||||
if (isConsolidatedBillingEnabled &&
|
||||
provider.Status == ProviderStatusType.Billable &&
|
||||
organization.Status == OrganizationStatusType.Managed &&
|
||||
if (provider.IsBillable() &&
|
||||
organization.IsValidClient() &&
|
||||
!string.IsNullOrEmpty(organization.GatewayCustomerId))
|
||||
{
|
||||
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
||||
|
@ -8,7 +8,6 @@ using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@ -101,24 +100,16 @@ public class ProviderService : IProviderService
|
||||
throw new BadRequestException("Invalid owner.");
|
||||
}
|
||||
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
||||
if (taxInfo == null || string.IsNullOrEmpty(taxInfo.BillingAddressCountry) || string.IsNullOrEmpty(taxInfo.BillingAddressPostalCode))
|
||||
{
|
||||
provider.Status = ProviderStatusType.Created;
|
||||
await _providerRepository.UpsertAsync(provider);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (taxInfo == null || string.IsNullOrEmpty(taxInfo.BillingAddressCountry) || string.IsNullOrEmpty(taxInfo.BillingAddressPostalCode))
|
||||
{
|
||||
throw new BadRequestException("Both address and postal code are required to set up your provider.");
|
||||
}
|
||||
var customer = await _providerBillingService.SetupCustomer(provider, taxInfo);
|
||||
provider.GatewayCustomerId = customer.Id;
|
||||
var subscription = await _providerBillingService.SetupSubscription(provider);
|
||||
provider.GatewaySubscriptionId = subscription.Id;
|
||||
provider.Status = ProviderStatusType.Billable;
|
||||
await _providerRepository.UpsertAsync(provider);
|
||||
throw new BadRequestException("Both address and postal code are required to set up your provider.");
|
||||
}
|
||||
var customer = await _providerBillingService.SetupCustomer(provider, taxInfo);
|
||||
provider.GatewayCustomerId = customer.Id;
|
||||
var subscription = await _providerBillingService.SetupSubscription(provider);
|
||||
provider.GatewaySubscriptionId = subscription.Id;
|
||||
provider.Status = ProviderStatusType.Billable;
|
||||
await _providerRepository.UpsertAsync(provider);
|
||||
|
||||
providerUser.Key = key;
|
||||
await _providerUserRepository.ReplaceAsync(providerUser);
|
||||
@ -545,13 +536,9 @@ public class ProviderService : IProviderService
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||
|
||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && provider.IsBillable();
|
||||
ThrowOnInvalidPlanType(provider.Type, organizationSignup.Plan);
|
||||
|
||||
ThrowOnInvalidPlanType(provider.Type, organizationSignup.Plan, consolidatedBillingEnabled);
|
||||
|
||||
var (organization, _, defaultCollection) = consolidatedBillingEnabled
|
||||
? await _organizationService.SignupClientAsync(organizationSignup)
|
||||
: await _organizationService.SignUpAsync(organizationSignup);
|
||||
var (organization, _, defaultCollection) = await _organizationService.SignupClientAsync(organizationSignup);
|
||||
|
||||
var providerOrganization = new ProviderOrganization
|
||||
{
|
||||
@ -687,27 +674,24 @@ public class ProviderService : IProviderService
|
||||
return confirmedOwnersIds.Except(providerUserIds).Any();
|
||||
}
|
||||
|
||||
private void ThrowOnInvalidPlanType(ProviderType providerType, PlanType requestedType, bool consolidatedBillingEnabled = false)
|
||||
private void ThrowOnInvalidPlanType(ProviderType providerType, PlanType requestedType)
|
||||
{
|
||||
if (consolidatedBillingEnabled)
|
||||
switch (providerType)
|
||||
{
|
||||
switch (providerType)
|
||||
{
|
||||
case ProviderType.Msp:
|
||||
if (requestedType is not (PlanType.TeamsMonthly or PlanType.EnterpriseMonthly))
|
||||
{
|
||||
throw new BadRequestException($"Managed Service Providers cannot manage organizations with the plan type {requestedType}. Only Teams (Monthly) and Enterprise (Monthly) are allowed.");
|
||||
}
|
||||
break;
|
||||
case ProviderType.MultiOrganizationEnterprise:
|
||||
if (requestedType is not (PlanType.EnterpriseMonthly or PlanType.EnterpriseAnnually))
|
||||
{
|
||||
throw new BadRequestException($"Multi-organization Enterprise Providers cannot manage organizations with the plan type {requestedType}. Only Enterprise (Monthly) and Enterprise (Annually) are allowed.");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestException($"Unsupported provider type {providerType}.");
|
||||
}
|
||||
case ProviderType.Msp:
|
||||
if (requestedType is not (PlanType.TeamsMonthly or PlanType.EnterpriseMonthly))
|
||||
{
|
||||
throw new BadRequestException($"Managed Service Providers cannot manage organizations with the plan type {requestedType}. Only Teams (Monthly) and Enterprise (Monthly) are allowed.");
|
||||
}
|
||||
break;
|
||||
case ProviderType.MultiOrganizationEnterprise:
|
||||
if (requestedType is not (PlanType.EnterpriseMonthly or PlanType.EnterpriseAnnually))
|
||||
{
|
||||
throw new BadRequestException($"Multi-organization Enterprise Providers cannot manage organizations with the plan type {requestedType}. Only Enterprise (Monthly) and Enterprise (Annually) are allowed.");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestException($"Unsupported provider type {providerType}.");
|
||||
}
|
||||
|
||||
if (ProviderDisallowedOrganizationTypes.Contains(requestedType))
|
||||
|
@ -2,17 +2,14 @@
|
||||
using Bit.Commercial.Core.Billing.Models;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
@ -27,7 +24,6 @@ using Stripe;
|
||||
namespace Bit.Commercial.Core.Billing;
|
||||
|
||||
public class ProviderBillingService(
|
||||
ICurrentContext currentContext,
|
||||
IGlobalSettings globalSettings,
|
||||
ILogger<ProviderBillingService> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -35,38 +31,76 @@ public class ProviderBillingService(
|
||||
IProviderInvoiceItemRepository providerInvoiceItemRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IProviderPlanRepository providerPlanRepository,
|
||||
IProviderRepository providerRepository,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ISubscriberService subscriberService) : IProviderBillingService
|
||||
{
|
||||
public async Task AssignSeatsToClientOrganization(
|
||||
Provider provider,
|
||||
Organization organization,
|
||||
int seats)
|
||||
public async Task ChangePlan(ChangeProviderPlanCommand command)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(organization);
|
||||
var plan = await providerPlanRepository.GetByIdAsync(command.ProviderPlanId);
|
||||
|
||||
if (seats < 0)
|
||||
if (plan == null)
|
||||
{
|
||||
throw new BillingException(
|
||||
"You cannot assign negative seats to a client.",
|
||||
"MSP cannot assign negative seats to a client organization");
|
||||
throw new BadRequestException("Provider plan not found.");
|
||||
}
|
||||
|
||||
if (seats == organization.Seats)
|
||||
if (plan.PlanType == command.NewPlan)
|
||||
{
|
||||
logger.LogWarning("Client organization ({ID}) already has {Seats} seats assigned to it", organization.Id, organization.Seats);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var seatAdjustment = seats - (organization.Seats ?? 0);
|
||||
var oldPlanConfiguration = StaticStore.GetPlan(plan.PlanType);
|
||||
|
||||
await ScaleSeats(provider, organization.PlanType, seatAdjustment);
|
||||
plan.PlanType = command.NewPlan;
|
||||
await providerPlanRepository.ReplaceAsync(plan);
|
||||
|
||||
organization.Seats = seats;
|
||||
Subscription subscription;
|
||||
try
|
||||
{
|
||||
subscription = await stripeAdapter.ProviderSubscriptionGetAsync(command.GatewaySubscriptionId, plan.ProviderId);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
throw new ConflictException("Subscription not found.");
|
||||
}
|
||||
|
||||
await organizationRepository.ReplaceAsync(organization);
|
||||
var oldSubscriptionItem = subscription.Items.SingleOrDefault(x =>
|
||||
x.Price.Id == oldPlanConfiguration.PasswordManager.StripeProviderPortalSeatPlanId);
|
||||
|
||||
var updateOptions = new SubscriptionUpdateOptions
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new SubscriptionItemOptions
|
||||
{
|
||||
Price = StaticStore.GetPlan(command.NewPlan).PasswordManager.StripeProviderPortalSeatPlanId,
|
||||
Quantity = oldSubscriptionItem!.Quantity
|
||||
},
|
||||
new SubscriptionItemOptions
|
||||
{
|
||||
Id = oldSubscriptionItem.Id,
|
||||
Deleted = true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId, updateOptions);
|
||||
|
||||
// Refactor later to ?ChangeClientPlanCommand? (ProviderPlanId, ProviderId, OrganizationId)
|
||||
// 1. Retrieve PlanType and PlanName for ProviderPlan
|
||||
// 2. Assign PlanType & PlanName to Organization
|
||||
var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(plan.ProviderId);
|
||||
|
||||
foreach (var providerOrganization in providerOrganizations)
|
||||
{
|
||||
var organization = await organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new ConflictException($"Organization '{providerOrganization.Id}' not found.");
|
||||
}
|
||||
organization.PlanType = command.NewPlan;
|
||||
organization.Plan = StaticStore.GetPlan(command.NewPlan).Name;
|
||||
await organizationRepository.ReplaceAsync(organization);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateCustomerForClientOrganization(
|
||||
@ -171,65 +205,16 @@ public class ProviderBillingService(
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
|
||||
public async Task<int> GetAssignedSeatTotalForPlanOrThrow(
|
||||
Guid providerId,
|
||||
PlanType planType)
|
||||
{
|
||||
var provider = await providerRepository.GetByIdAsync(providerId);
|
||||
|
||||
if (provider == null)
|
||||
{
|
||||
logger.LogError(
|
||||
"Could not find provider ({ID}) when retrieving assigned seat total",
|
||||
providerId);
|
||||
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
if (provider.Type == ProviderType.Reseller)
|
||||
{
|
||||
logger.LogError("Assigned seats cannot be retrieved for reseller-type provider ({ID})", providerId);
|
||||
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId);
|
||||
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
|
||||
return providerOrganizations
|
||||
.Where(providerOrganization => providerOrganization.Plan == plan.Name && providerOrganization.Status == OrganizationStatusType.Managed)
|
||||
.Sum(providerOrganization => providerOrganization.Seats ?? 0);
|
||||
}
|
||||
|
||||
public async Task ScaleSeats(
|
||||
Provider provider,
|
||||
PlanType planType,
|
||||
int seatAdjustment)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(provider);
|
||||
var providerPlan = await GetProviderPlanAsync(provider, planType);
|
||||
|
||||
if (!provider.SupportsConsolidatedBilling())
|
||||
{
|
||||
logger.LogError("Provider ({ProviderID}) cannot scale their seats", provider.Id);
|
||||
var seatMinimum = providerPlan.SeatMinimum ?? 0;
|
||||
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
||||
|
||||
var providerPlan = providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == planType);
|
||||
|
||||
if (providerPlan == null || !providerPlan.IsConfigured())
|
||||
{
|
||||
logger.LogError("Cannot scale provider ({ProviderID}) seats for plan type {PlanType} when their matching provider plan is not configured", provider.Id, planType);
|
||||
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
var seatMinimum = providerPlan.SeatMinimum.GetValueOrDefault(0);
|
||||
|
||||
var currentlyAssignedSeatTotal = await GetAssignedSeatTotalForPlanOrThrow(provider.Id, planType);
|
||||
var currentlyAssignedSeatTotal = await GetAssignedSeatTotalAsync(provider, planType);
|
||||
|
||||
var newlyAssignedSeatTotal = currentlyAssignedSeatTotal + seatAdjustment;
|
||||
|
||||
@ -256,13 +241,6 @@ public class ProviderBillingService(
|
||||
else if (currentlyAssignedSeatTotal <= seatMinimum &&
|
||||
newlyAssignedSeatTotal > seatMinimum)
|
||||
{
|
||||
if (!currentContext.ProviderProviderAdmin(provider.Id))
|
||||
{
|
||||
logger.LogError("Service user for provider ({ProviderID}) cannot scale a provider's seat count over the seat minimum", provider.Id);
|
||||
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
await update(
|
||||
seatMinimum,
|
||||
newlyAssignedSeatTotal);
|
||||
@ -291,6 +269,26 @@ public class ProviderBillingService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SeatAdjustmentResultsInPurchase(
|
||||
Provider provider,
|
||||
PlanType planType,
|
||||
int seatAdjustment)
|
||||
{
|
||||
var providerPlan = await GetProviderPlanAsync(provider, planType);
|
||||
|
||||
var seatMinimum = providerPlan.SeatMinimum;
|
||||
|
||||
var currentlyAssignedSeatTotal = await GetAssignedSeatTotalAsync(provider, planType);
|
||||
|
||||
var newlyAssignedSeatTotal = currentlyAssignedSeatTotal + seatAdjustment;
|
||||
|
||||
return
|
||||
// Below the limit to above the limit
|
||||
(currentlyAssignedSeatTotal <= seatMinimum && newlyAssignedSeatTotal > seatMinimum) ||
|
||||
// Above the limit to further above the limit
|
||||
(currentlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > currentlyAssignedSeatTotal);
|
||||
}
|
||||
|
||||
public async Task<Customer> SetupCustomer(
|
||||
Provider provider,
|
||||
TaxInfo taxInfo)
|
||||
@ -431,75 +429,6 @@ public class ProviderBillingService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ChangePlan(ChangeProviderPlanCommand command)
|
||||
{
|
||||
var plan = await providerPlanRepository.GetByIdAsync(command.ProviderPlanId);
|
||||
|
||||
if (plan == null)
|
||||
{
|
||||
throw new BadRequestException("Provider plan not found.");
|
||||
}
|
||||
|
||||
if (plan.PlanType == command.NewPlan)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var oldPlanConfiguration = StaticStore.GetPlan(plan.PlanType);
|
||||
|
||||
plan.PlanType = command.NewPlan;
|
||||
await providerPlanRepository.ReplaceAsync(plan);
|
||||
|
||||
Subscription subscription;
|
||||
try
|
||||
{
|
||||
subscription = await stripeAdapter.ProviderSubscriptionGetAsync(command.GatewaySubscriptionId, plan.ProviderId);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
throw new ConflictException("Subscription not found.");
|
||||
}
|
||||
|
||||
var oldSubscriptionItem = subscription.Items.SingleOrDefault(x =>
|
||||
x.Price.Id == oldPlanConfiguration.PasswordManager.StripeProviderPortalSeatPlanId);
|
||||
|
||||
var updateOptions = new SubscriptionUpdateOptions
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new SubscriptionItemOptions
|
||||
{
|
||||
Price = StaticStore.GetPlan(command.NewPlan).PasswordManager.StripeProviderPortalSeatPlanId,
|
||||
Quantity = oldSubscriptionItem!.Quantity
|
||||
},
|
||||
new SubscriptionItemOptions
|
||||
{
|
||||
Id = oldSubscriptionItem.Id,
|
||||
Deleted = true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId, updateOptions);
|
||||
|
||||
// Refactor later to ?ChangeClientPlanCommand? (ProviderPlanId, ProviderId, OrganizationId)
|
||||
// 1. Retrieve PlanType and PlanName for ProviderPlan
|
||||
// 2. Assign PlanType & PlanName to Organization
|
||||
var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(plan.ProviderId);
|
||||
|
||||
foreach (var providerOrganization in providerOrganizations)
|
||||
{
|
||||
var organization = await organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new ConflictException($"Organization '{providerOrganization.Id}' not found.");
|
||||
}
|
||||
organization.PlanType = command.NewPlan;
|
||||
organization.Plan = StaticStore.GetPlan(command.NewPlan).Name;
|
||||
await organizationRepository.ReplaceAsync(organization);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command)
|
||||
{
|
||||
if (command.Configuration.Any(x => x.SeatsMinimum < 0))
|
||||
@ -610,4 +539,32 @@ public class ProviderBillingService(
|
||||
|
||||
await providerPlanRepository.ReplaceAsync(providerPlan);
|
||||
};
|
||||
|
||||
// TODO: Replace with SPROC
|
||||
private async Task<int> GetAssignedSeatTotalAsync(Provider provider, PlanType planType)
|
||||
{
|
||||
var providerOrganizations =
|
||||
await providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id);
|
||||
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
|
||||
return providerOrganizations
|
||||
.Where(providerOrganization => providerOrganization.Plan == plan.Name && providerOrganization.Status == OrganizationStatusType.Managed)
|
||||
.Sum(providerOrganization => providerOrganization.Seats ?? 0);
|
||||
}
|
||||
|
||||
// TODO: Replace with SPROC
|
||||
private async Task<ProviderPlan> GetProviderPlanAsync(Provider provider, PlanType planType)
|
||||
{
|
||||
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
||||
|
||||
var providerPlan = providerPlans.FirstOrDefault(x => x.PlanType == planType);
|
||||
|
||||
if (providerPlan == null || !providerPlan.IsConfigured())
|
||||
{
|
||||
throw new BillingException(message: "Provider plan is missing or misconfigured");
|
||||
}
|
||||
|
||||
return providerPlan;
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
@RenderBody()
|
||||
</div>
|
||||
|
||||
<div class="container footer text-muted">
|
||||
<div class="container footer text-body-secondary">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
© @DateTime.Now.Year, Bitwarden Inc.
|
||||
|
37
bitwarden_license/src/Sso/package-lock.json
generated
37
bitwarden_license/src/Sso/package-lock.json
generated
@ -9,10 +9,9 @@
|
||||
"version": "0.0.0",
|
||||
"license": "-",
|
||||
"dependencies": {
|
||||
"bootstrap": "4.6.2",
|
||||
"bootstrap": "5.3.3",
|
||||
"font-awesome": "4.7.0",
|
||||
"jquery": "3.7.1",
|
||||
"popper.js": "1.16.1"
|
||||
"jquery": "3.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"css-loader": "7.1.2",
|
||||
@ -384,6 +383,17 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
@ -702,9 +712,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
|
||||
"integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==",
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
|
||||
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@ -715,10 +725,8 @@
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"jquery": "1.9.1 - 3",
|
||||
"popper.js": "^1.16.1"
|
||||
"@popperjs/core": "^2.11.8"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
@ -1577,17 +1585,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/popper.js": {
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
|
||||
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
|
||||
"deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.47",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||
|
@ -8,10 +8,9 @@
|
||||
"build": "webpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "4.6.2",
|
||||
"bootstrap": "5.3.3",
|
||||
"font-awesome": "4.7.0",
|
||||
"jquery": "3.7.1",
|
||||
"popper.js": "1.16.1"
|
||||
"jquery": "3.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"css-loader": "7.1.2",
|
||||
|
@ -13,8 +13,6 @@ module.exports = {
|
||||
entry: {
|
||||
site: [
|
||||
path.resolve(__dirname, paths.sassDir, "site.scss"),
|
||||
|
||||
"popper.js",
|
||||
"bootstrap",
|
||||
"jquery",
|
||||
"font-awesome/css/font-awesome.css",
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Bit.Commercial.Core.AdminConsole.Providers;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
@ -155,9 +154,6 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
"b@example.com"
|
||||
]);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
|
||||
.Returns(GetSubscription(organization.GatewaySubscriptionId));
|
||||
|
||||
@ -222,9 +218,6 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
"b@example.com"
|
||||
]);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
||||
.Returns(true);
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
|
||||
|
@ -1,6 +1,5 @@
|
||||
using Bit.Commercial.Core.AdminConsole.Services;
|
||||
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
@ -55,36 +54,8 @@ public class ProviderServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key,
|
||||
[ProviderUser(ProviderUserStatusType.Confirmed, ProviderUserType.ProviderAdmin)] ProviderUser providerUser,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
providerUser.ProviderId = provider.Id;
|
||||
providerUser.UserId = user.Id;
|
||||
var userService = sutProvider.GetDependency<IUserService>();
|
||||
userService.GetUserByIdAsync(user.Id).Returns(user);
|
||||
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
|
||||
|
||||
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
|
||||
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
||||
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
|
||||
.Returns(protector);
|
||||
sutProvider.Create();
|
||||
|
||||
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||
|
||||
await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key);
|
||||
|
||||
await sutProvider.GetDependency<IProviderRepository>().Received().UpsertAsync(provider);
|
||||
await sutProvider.GetDependency<IProviderUserRepository>().Received()
|
||||
.ReplaceAsync(Arg.Is<ProviderUser>(pu => pu.UserId == user.Id && pu.ProviderId == provider.Id && pu.Key == key));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CompleteSetupAsync_ConsolidatedBilling_Success(User user, Provider provider, string key, TaxInfo taxInfo,
|
||||
[ProviderUser(ProviderUserStatusType.Confirmed, ProviderUserType.ProviderAdmin)] ProviderUser providerUser,
|
||||
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TaxInfo taxInfo,
|
||||
[ProviderUser] ProviderUser providerUser,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
providerUser.ProviderId = provider.Id;
|
||||
@ -100,9 +71,6 @@ public class ProviderServiceTests
|
||||
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
|
||||
.Returns(protector);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
||||
.Returns(true);
|
||||
|
||||
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
|
||||
|
||||
var customer = new Customer { Id = "customer_id" };
|
||||
@ -489,7 +457,7 @@ public class ProviderServiceTests
|
||||
public async Task AddOrganization_OrganizationHasSecretsManager_Throws(Provider provider, Organization organization, string key,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
organization.PlanType = PlanType.EnterpriseMonthly;
|
||||
organization.UseSecretsManager = true;
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
@ -506,7 +474,7 @@ public class ProviderServiceTests
|
||||
public async Task AddOrganization_Success(Provider provider, Organization organization, string key,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
organization.PlanType = PlanType.EnterpriseMonthly;
|
||||
|
||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||
@ -549,8 +517,8 @@ public class ProviderServiceTests
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
var expectedPlanType = PlanType.EnterpriseAnnually;
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
var expectedPlanType = PlanType.EnterpriseMonthly;
|
||||
organization.PlanType = PlanType.EnterpriseMonthly;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
||||
@ -579,12 +547,12 @@ public class ProviderServiceTests
|
||||
BackdateProviderCreationDate(provider, newCreationDate);
|
||||
provider.Type = ProviderType.Msp;
|
||||
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
organization.Plan = "Enterprise (Annually)";
|
||||
organization.PlanType = PlanType.EnterpriseMonthly;
|
||||
organization.Plan = "Enterprise (Monthly)";
|
||||
|
||||
var expectedPlanType = PlanType.EnterpriseAnnually2020;
|
||||
var expectedPlanType = PlanType.EnterpriseMonthly2020;
|
||||
|
||||
var expectedPlanId = "2020-enterprise-org-seat-annually";
|
||||
var expectedPlanId = "2020-enterprise-org-seat-monthly";
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
@ -663,11 +631,11 @@ public class ProviderServiceTests
|
||||
public async Task CreateOrganizationAsync_Success(Provider provider, OrganizationSignup organizationSignup,
|
||||
Organization organization, string clientOwnerEmail, User user, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
organizationSignup.Plan = PlanType.EnterpriseAnnually;
|
||||
organizationSignup.Plan = PlanType.EnterpriseMonthly;
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup)
|
||||
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
||||
.Returns((organization, null as OrganizationUser, new Collection()));
|
||||
|
||||
var providerOrganization =
|
||||
@ -688,7 +656,7 @@ public class ProviderServiceTests
|
||||
}
|
||||
|
||||
[Theory, OrganizationCustomize, BitAutoData]
|
||||
public async Task CreateOrganizationAsync_ConsolidatedBillingEnabled_InvalidPlanType_ThrowsBadRequestException(
|
||||
public async Task CreateOrganizationAsync_InvalidPlanType_ThrowsBadRequestException(
|
||||
Provider provider,
|
||||
OrganizationSignup organizationSignup,
|
||||
Organization organization,
|
||||
@ -696,8 +664,6 @@ public class ProviderServiceTests
|
||||
User user,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
||||
|
||||
provider.Type = ProviderType.Msp;
|
||||
provider.Status = ProviderStatusType.Billable;
|
||||
|
||||
@ -717,7 +683,7 @@ public class ProviderServiceTests
|
||||
}
|
||||
|
||||
[Theory, OrganizationCustomize, BitAutoData]
|
||||
public async Task CreateOrganizationAsync_ConsolidatedBillingEnabled_InvokeSignupClientAsync(
|
||||
public async Task CreateOrganizationAsync_InvokeSignupClientAsync(
|
||||
Provider provider,
|
||||
OrganizationSignup organizationSignup,
|
||||
Organization organization,
|
||||
@ -725,8 +691,6 @@ public class ProviderServiceTests
|
||||
User user,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
||||
|
||||
provider.Type = ProviderType.Msp;
|
||||
provider.Status = ProviderStatusType.Billable;
|
||||
|
||||
@ -771,11 +735,11 @@ public class ProviderServiceTests
|
||||
(Provider provider, OrganizationSignup organizationSignup, Organization organization, string clientOwnerEmail,
|
||||
User user, SutProvider<ProviderService> sutProvider, Collection defaultCollection)
|
||||
{
|
||||
organizationSignup.Plan = PlanType.EnterpriseAnnually;
|
||||
organizationSignup.Plan = PlanType.EnterpriseMonthly;
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup)
|
||||
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
||||
.Returns((organization, null as OrganizationUser, defaultCollection));
|
||||
|
||||
var providerOrganization =
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -5,8 +5,10 @@ using Bit.Admin.Services;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
@ -53,8 +55,8 @@ public class OrganizationsController : Controller
|
||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public OrganizationsController(
|
||||
IOrganizationService organizationService,
|
||||
@ -80,8 +82,8 @@ public class OrganizationsController : Controller
|
||||
IServiceAccountRepository serviceAccountRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
||||
IFeatureService featureService,
|
||||
IProviderBillingService providerBillingService)
|
||||
IProviderBillingService providerBillingService,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_organizationService = organizationService;
|
||||
_organizationRepository = organizationRepository;
|
||||
@ -106,8 +108,8 @@ public class OrganizationsController : Controller
|
||||
_serviceAccountRepository = serviceAccountRepository;
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
|
||||
_featureService = featureService;
|
||||
_providerBillingService = providerBillingService;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Org_List_View)]
|
||||
@ -230,7 +232,23 @@ public class OrganizationsController : Controller
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IActionResult> Edit(Guid id, OrganizationEditModel model)
|
||||
{
|
||||
var organization = await GetOrganization(id, model);
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
TempData["Error"] = "Could not find organization to update.";
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
var existingOrganizationData = new Organization
|
||||
{
|
||||
Id = organization.Id,
|
||||
Status = organization.Status,
|
||||
PlanType = organization.PlanType,
|
||||
Seats = organization.Seats
|
||||
};
|
||||
|
||||
UpdateOrganization(organization, model);
|
||||
|
||||
if (organization.UseSecretsManager &&
|
||||
!StaticStore.GetPlan(organization.PlanType).SupportsSecretsManager)
|
||||
@ -239,7 +257,12 @@ public class OrganizationsController : Controller
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
|
||||
await HandlePotentialProviderSeatScalingAsync(
|
||||
existingOrganizationData,
|
||||
model);
|
||||
|
||||
await _organizationRepository.ReplaceAsync(organization);
|
||||
|
||||
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.OrganizationEditedByAdmin, organization, _currentContext)
|
||||
{
|
||||
@ -262,9 +285,7 @@ public class OrganizationsController : Controller
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
|
||||
if (consolidatedBillingEnabled && organization.IsValidClient())
|
||||
if (organization.IsValidClient())
|
||||
{
|
||||
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||
|
||||
@ -394,10 +415,9 @@ public class OrganizationsController : Controller
|
||||
|
||||
return Json(null);
|
||||
}
|
||||
private async Task<Organization> GetOrganization(Guid id, OrganizationEditModel model)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
|
||||
private void UpdateOrganization(Organization organization, OrganizationEditModel model)
|
||||
{
|
||||
if (_accessControlService.UserHasPermission(Permission.Org_CheckEnabledBox))
|
||||
{
|
||||
organization.Enabled = model.Enabled;
|
||||
@ -449,7 +469,62 @@ public class OrganizationsController : Controller
|
||||
organization.GatewayCustomerId = model.GatewayCustomerId;
|
||||
organization.GatewaySubscriptionId = model.GatewaySubscriptionId;
|
||||
}
|
||||
}
|
||||
|
||||
return organization;
|
||||
private async Task HandlePotentialProviderSeatScalingAsync(
|
||||
Organization organization,
|
||||
OrganizationEditModel update)
|
||||
{
|
||||
var scaleMSPOnClientOrganizationUpdate =
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate);
|
||||
|
||||
if (!scaleMSPOnClientOrganizationUpdate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||
|
||||
// No scaling required
|
||||
if (provider is not { Type: ProviderType.Msp, Status: ProviderStatusType.Billable } ||
|
||||
organization is not { Status: OrganizationStatusType.Managed } ||
|
||||
!organization.Seats.HasValue ||
|
||||
update is { Seats: null, PlanType: null } ||
|
||||
update is { PlanType: not PlanType.TeamsMonthly and not PlanType.EnterpriseMonthly } ||
|
||||
(PlanTypesMatch() && SeatsMatch()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Only scale the plan
|
||||
if (!PlanTypesMatch() && SeatsMatch())
|
||||
{
|
||||
await _providerBillingService.ScaleSeats(provider, organization.PlanType, -organization.Seats.Value);
|
||||
await _providerBillingService.ScaleSeats(provider, update.PlanType!.Value, organization.Seats.Value);
|
||||
}
|
||||
// Only scale the seats
|
||||
else if (PlanTypesMatch() && !SeatsMatch())
|
||||
{
|
||||
var seatAdjustment = update.Seats!.Value - organization.Seats.Value;
|
||||
await _providerBillingService.ScaleSeats(provider, organization.PlanType, seatAdjustment);
|
||||
}
|
||||
// Scale both
|
||||
else if (!PlanTypesMatch() && !SeatsMatch())
|
||||
{
|
||||
var seatAdjustment = update.Seats!.Value - organization.Seats.Value;
|
||||
var planTypeAdjustment = organization.Seats.Value;
|
||||
var totalAdjustment = seatAdjustment + planTypeAdjustment;
|
||||
|
||||
await _providerBillingService.ScaleSeats(provider, organization.PlanType, -organization.Seats.Value);
|
||||
await _providerBillingService.ScaleSeats(provider, update.PlanType!.Value, totalAdjustment);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
bool PlanTypesMatch()
|
||||
=> update.PlanType.HasValue && update.PlanType.Value == organization.PlanType;
|
||||
|
||||
bool SeatsMatch()
|
||||
=> update.Seats.HasValue && update.Seats.Value == organization.Seats;
|
||||
}
|
||||
}
|
||||
|
@ -282,9 +282,7 @@ public class ProvidersController : Controller
|
||||
await _providerRepository.ReplaceAsync(provider);
|
||||
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
|
||||
|
||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
|
||||
if (!isConsolidatedBillingEnabled || !provider.IsBillable())
|
||||
if (!provider.IsBillable())
|
||||
{
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
@ -340,10 +338,7 @@ public class ProvidersController : Controller
|
||||
var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);
|
||||
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
|
||||
|
||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
|
||||
|
||||
if (!isConsolidatedBillingEnabled || !provider.IsBillable())
|
||||
if (!provider.IsBillable())
|
||||
{
|
||||
return new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>());
|
||||
}
|
||||
|
@ -103,19 +103,19 @@
|
||||
|
||||
<div class="d-flex mt-4">
|
||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
||||
<div class="ml-auto d-flex">
|
||||
<div class="ms-auto d-flex">
|
||||
@if (canInitiateTrial && Model.Provider is null)
|
||||
{
|
||||
<button class="btn btn-secondary mr-2" type="button" id="teams-trial">
|
||||
<button class="btn btn-secondary me-2" type="button" id="teams-trial">
|
||||
Teams Trial
|
||||
</button>
|
||||
<button class="btn btn-secondary mr-2" type="button" id="enterprise-trial">
|
||||
<button class="btn btn-secondary me-2" type="button" id="enterprise-trial">
|
||||
Enterprise Trial
|
||||
</button>
|
||||
}
|
||||
@if (canUnlinkFromProvider && Model.Provider is not null)
|
||||
{
|
||||
<button class="btn btn-outline-danger mr-2"
|
||||
<button class="btn btn-outline-danger me-2"
|
||||
onclick="return unlinkProvider('@Model.Organization.Id');">
|
||||
Unlink provider
|
||||
</button>
|
||||
@ -124,7 +124,7 @@
|
||||
{
|
||||
<form asp-action="DeleteInitiation" asp-route-id="@Model.Organization.Id" id="initiate-delete-form">
|
||||
<input type="hidden" name="AdminEmail" id="AdminEmail" />
|
||||
<button class="btn btn-danger mr-2" type="submit">Request Delete</button>
|
||||
<button class="btn btn-danger me-2" type="submit">Request Delete</button>
|
||||
</form>
|
||||
<form asp-action="Delete" asp-route-id="@Model.Organization.Id"
|
||||
onsubmit="return confirm('Are you sure you want to hard delete this organization?')">
|
||||
|
@ -5,21 +5,31 @@
|
||||
|
||||
<h1>Organizations</h1>
|
||||
|
||||
<form class="form-inline mb-2" method="get">
|
||||
<label class="sr-only" asp-for="Name">Name</label>
|
||||
<input type="text" class="form-control mb-2 mr-2" placeholder="Name" asp-for="Name" name="name">
|
||||
<label class="sr-only" asp-for="UserEmail">User email</label>
|
||||
<input type="text" class="form-control mb-2 mr-2" placeholder="User email" asp-for="UserEmail" name="userEmail">
|
||||
<form class="row row-cols-lg-auto g-3 align-items-center mb-2" method="get">
|
||||
<div class="col-12">
|
||||
<label class="visually-hidden" asp-for="Name">Name</label>
|
||||
<input type="text" class="form-control" placeholder="Name" asp-for="Name" name="name">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="visually-hidden" asp-for="UserEmail">User email</label>
|
||||
<input type="text" class="form-control" placeholder="User email" asp-for="UserEmail" name="userEmail">
|
||||
</div>
|
||||
@if(!Model.SelfHosted)
|
||||
{
|
||||
<label class="sr-only" asp-for="Paid">Customer</label>
|
||||
<select class="form-control mb-2 mr-2" asp-for="Paid" name="paid">
|
||||
<option asp-selected="!Model.Paid.HasValue" value="">-- Customer --</option>
|
||||
<option asp-selected="Model.Paid.GetValueOrDefault(false)" value="true">Paid</option>
|
||||
<option asp-selected="!Model.Paid.GetValueOrDefault(true)" value="false">Freeloader</option>
|
||||
</select>
|
||||
<div class="col-12">
|
||||
<label class="visually-hidden" asp-for="Paid">Customer</label>
|
||||
<select class="form-select" asp-for="Paid" name="paid">
|
||||
<option asp-selected="!Model.Paid.HasValue" value="">-- Customer --</option>
|
||||
<option asp-selected="Model.Paid.GetValueOrDefault(false)" value="true">Paid</option>
|
||||
<option asp-selected="!Model.Paid.GetValueOrDefault(true)" value="false">Freeloader</option>
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
<button type="submit" class="btn btn-primary mb-2" title="Search"><i class="fa fa-search"></i> Search</button>
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary" title="Search">
|
||||
<i class="fa fa-search"></i> Search
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
@ -68,7 +78,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fa fa-smile-o fa-lg fa-fw text-muted" title="Freeloader"></i>
|
||||
<i class="fa fa-smile-o fa-lg fa-fw text-body-secondary" title="Freeloader"></i>
|
||||
}
|
||||
}
|
||||
@if(org.MaxStorageGb.HasValue && org.MaxStorageGb > 1)
|
||||
@ -78,7 +88,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fa fa-plus-square-o fa-lg fa-fw text-muted"
|
||||
<i class="fa fa-plus-square-o fa-lg fa-fw text-body-secondary"
|
||||
title="No Additional Storage"></i>
|
||||
}
|
||||
@if(org.Enabled)
|
||||
@ -88,7 +98,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fa fa-times-circle-o fa-lg fa-fw text-muted" title="Disabled"></i>
|
||||
<i class="fa fa-times-circle-o fa-lg fa-fw text-body-secondary" title="Disabled"></i>
|
||||
}
|
||||
@if(org.TwoFactorIsEnabled())
|
||||
{
|
||||
@ -96,7 +106,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fa fa-unlock fa-lg fa-fw text-muted" title="2FA Not Enabled"></i>
|
||||
<i class="fa fa-unlock fa-lg fa-fw text-body-secondary" title="2FA Not Enabled"></i>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -9,12 +9,18 @@
|
||||
<h1>Add Existing Organization</h1>
|
||||
<div class="row mb-2">
|
||||
<div class="col">
|
||||
<form class="form-inline mb-2" method="get" asp-route-id="@providerId">
|
||||
<label class="sr-only" asp-for="OrganizationName"></label>
|
||||
<input type="text" class="form-control mb-2 mr-2 flex-fill" placeholder="@Html.DisplayNameFor(m => m.OrganizationName)" asp-for="OrganizationName" name="name">
|
||||
<label class="sr-only" asp-for="OrganizationOwnerEmail"></label>
|
||||
<input type="email" class="form-control mb-2 mr-2 flex-fill" placeholder="@Html.DisplayNameFor(m => m.OrganizationOwnerEmail)" asp-for="OrganizationOwnerEmail" name="ownerEmail">
|
||||
<button type="submit" class="btn btn-primary mb-2" title="Search" formmethod="get"><i class="fa fa-search"></i> Search</button>
|
||||
<form class="row g-3 align-items-center mb-2" method="get" asp-route-id="@providerId">
|
||||
<div class="col">
|
||||
<label class="visually-hidden" asp-for="OrganizationName"></label>
|
||||
<input type="text" class="form-control" placeholder="@Html.DisplayNameFor(m => m.OrganizationName)" asp-for="OrganizationName" name="name">
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="visually-hidden" asp-for="OrganizationOwnerEmail"></label>
|
||||
<input type="email" class="form-control" placeholder="@Html.DisplayNameFor(m => m.OrganizationOwnerEmail)" asp-for="OrganizationOwnerEmail" name="ownerEmail">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="submit" class="btn btn-primary" title="Search" formmethod="get"><i class="fa fa-search"></i> Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -22,23 +22,23 @@
|
||||
<h1>Create Provider</h1>
|
||||
<form method="post" asp-action="Create">
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Type" class="h2"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="Type" class="form-label h2"></label>
|
||||
@foreach (var providerType in providerTypes)
|
||||
{
|
||||
var providerTypeValue = (int)providerType;
|
||||
<div class="form-group">
|
||||
<div class="mb-3">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
@Html.RadioButtonFor(m => m.Type, providerType, new { id = $"providerType-{providerTypeValue}", @class = "form-check-input" })
|
||||
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetName(), new { @class = "form-check-label align-middle", @for = $"providerType-{providerTypeValue}" })
|
||||
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetName(), new { @class = "form-check-label", @for = $"providerType-{providerTypeValue}" })
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetDescription(), new { @class = "form-check-label small text-muted align-top", @for = $"providerType-{providerTypeValue}" })
|
||||
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetDescription(), new { @class = "form-check-label small text-body-secondary ps-4", @for = $"providerType-{providerTypeValue}" })
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,39 +1,31 @@
|
||||
@using Bit.Core.AdminConsole.Enums.Provider
|
||||
@using Bit.Core
|
||||
|
||||
@model CreateMspProviderModel
|
||||
|
||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Create Managed Service Provider";
|
||||
}
|
||||
|
||||
<h1>Create Managed Service Provider</h1>
|
||||
<div>
|
||||
<form class="form-group" method="post" asp-action="CreateMsp">
|
||||
<form method="post" asp-action="CreateMsp">
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="OwnerEmail"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="OwnerEmail" class="form-label"></label>
|
||||
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||
</div>
|
||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="TeamsMonthlySeatMinimum" class="form-label"></label>
|
||||
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="EnterpriseMonthlySeatMinimum" class="form-label"></label>
|
||||
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -7,17 +7,17 @@
|
||||
ViewData["Title"] = "Create Multi-organization Enterprise Provider";
|
||||
}
|
||||
|
||||
<h1>Create Multi-organization Enterprise Provider</h1>
|
||||
<h1 class="mb-4">Create Multi-organization Enterprise Provider</h1>
|
||||
<div>
|
||||
<form class="form-group" method="post" asp-action="CreateMultiOrganizationEnterprise">
|
||||
<form method="post" asp-action="CreateMultiOrganizationEnterprise">
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="OwnerEmail"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="OwnerEmail" class="form-label"></label>
|
||||
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<div class="mb-3">
|
||||
@{
|
||||
var multiOrgPlans = new List<PlanType>
|
||||
{
|
||||
@ -25,19 +25,19 @@
|
||||
PlanType.EnterpriseMonthly
|
||||
};
|
||||
}
|
||||
<label asp-for="Plan"></label>
|
||||
<select class="form-control" asp-for="Plan" asp-items="Html.GetEnumSelectList(multiOrgPlans)">
|
||||
<label asp-for="Plan" class="form-label"></label>
|
||||
<select class="form-select" asp-for="Plan" asp-items="Html.GetEnumSelectList(multiOrgPlans)">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="EnterpriseSeatMinimum"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="EnterpriseSeatMinimum" class="form-label"></label>
|
||||
<input type="number" class="form-control" asp-for="EnterpriseSeatMinimum">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||
<button type="submit" class="btn btn-primary">Create Provider</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -18,7 +18,7 @@
|
||||
@await Html.PartialAsync("~/AdminConsole/Views/Shared/_OrganizationForm.cshtml", Model)
|
||||
<div class="d-flex mt-4">
|
||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
||||
<div class="ml-auto d-flex">
|
||||
<div class="ms-auto d-flex">
|
||||
<form asp-controller="Providers" asp-action="Edit" asp-route-id="@Model.Provider.Id"
|
||||
onsubmit="return confirm('Are you sure you want to cancel?')">
|
||||
<button class="btn btn-outline-secondary" type="submit">Cancel</button>
|
||||
|
@ -6,18 +6,18 @@
|
||||
|
||||
<h1>Create Reseller Provider</h1>
|
||||
<div>
|
||||
<form class="form-group" method="post" asp-action="CreateReseller">
|
||||
<form class="mb-3" method="post" asp-action="CreateReseller">
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Name"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="Name" class="form-label"></label>
|
||||
<input type="text" class="form-control" asp-for="Name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="BusinessName"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="BusinessName" class="form-label"></label>
|
||||
<input type="text" class="form-control" asp-for="BusinessName">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="BillingEmail"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="BillingEmail" class="form-label"></label>
|
||||
<input type="text" class="form-control" asp-for="BillingEmail">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||
|
@ -34,21 +34,21 @@
|
||||
<h2>Billing</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="BillingEmail"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="BillingEmail" class="form-label"></label>
|
||||
<input type="email" class="form-control" asp-for="BillingEmail" readonly='@(!canEdit)'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="BillingPhone"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="BillingPhone" class="form-label"></label>
|
||||
<input type="tel" class="form-control" asp-for="BillingPhone">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && Model.Provider.IsBillable())
|
||||
@if (Model.Provider.IsBillable())
|
||||
{
|
||||
switch (Model.Provider.Type)
|
||||
{
|
||||
@ -56,58 +56,18 @@
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="TeamsMonthlySeatMinimum" class="form-label"></label>
|
||||
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="EnterpriseMonthlySeatMinimum" class="form-label"></label>
|
||||
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<div class="form-group">
|
||||
<label asp-for="Gateway"></label>
|
||||
<select class="form-control" asp-for="Gateway" asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="GatewayCustomerId"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewayCustomerId">
|
||||
<div class="input-group-append">
|
||||
<a href="@Model.GatewayCustomerUrl" class="btn btn-secondary" target="_blank">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="GatewaySubscriptionId"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewaySubscriptionId">
|
||||
<div class="input-group-append">
|
||||
<a href="@Model.GatewaySubscriptionUrl" class="btn btn-secondary" target="_blank">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
}
|
||||
case ProviderType.MultiOrganizationEnterprise:
|
||||
@ -116,7 +76,7 @@
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<div class="mb-3">
|
||||
@{
|
||||
var multiOrgPlans = new List<PlanType>
|
||||
{
|
||||
@ -124,15 +84,15 @@
|
||||
PlanType.EnterpriseMonthly
|
||||
};
|
||||
}
|
||||
<label asp-for="Plan"></label>
|
||||
<label asp-for="Plan" class="form-label"></label>
|
||||
<select class="form-control" asp-for="Plan" asp-items="Html.GetEnumSelectList(multiOrgPlans)">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="EnterpriseMinimumSeats"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="EnterpriseMinimumSeats" class="form-label"></label>
|
||||
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
|
||||
</div>
|
||||
</div>
|
||||
@ -141,6 +101,40 @@
|
||||
break;
|
||||
}
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label asp-for="Gateway" class="form-label"></label>
|
||||
<select class="form-control" asp-for="Gateway" asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label asp-for="GatewayCustomerId" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewayCustomerId">
|
||||
<button class="btn btn-secondary" type="button" onclick="window.open('@Model.GatewayCustomerUrl', '_blank')">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label asp-for="GatewaySubscriptionId" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewaySubscriptionId">
|
||||
<button class="btn btn-secondary" type="button" onclick="window.open('@Model.GatewaySubscriptionUrl', '_blank')">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
@await Html.PartialAsync("Organizations", Model)
|
||||
@ -151,21 +145,21 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content rounded">
|
||||
<div class="p-3">
|
||||
<h4 class="font-weight-bolder" id="exampleModalLabel">Request provider deletion</h4>
|
||||
<h4 class="fw-bolder" id="exampleModalLabel">Request provider deletion</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<span class="font-weight-light">
|
||||
<span class="fw-light">
|
||||
Enter the email of the provider admin that will receive the request to delete the provider portal.
|
||||
</span>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<div class="mb-3">
|
||||
<label for="provider-email" class="col-form-label">Provider email</label>
|
||||
<input type="email" class="form-control" id="provider-email">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-pill" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger btn-pill" onclick="initiateDeleteProvider('@Model.Provider.Id')">Send email request</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -175,21 +169,21 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content rounded">
|
||||
<div class="p-3">
|
||||
<h4 class="font-weight-bolder" id="exampleModalLabel">Delete provider</h4>
|
||||
<h4 class="fw-bolder" id="exampleModalLabel">Delete provider</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<span class="font-weight-light">
|
||||
<span class="fw-light">
|
||||
This action is permanent and irreversible. Enter the provider name to complete deletion of the provider and associated data.
|
||||
</span>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<div class="mb-3">
|
||||
<label for="provider-name" class="col-form-label">Provider name</label>
|
||||
<input type="text" class="form-control" id="provider-name">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-pill" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger btn-pill" onclick="deleteProvider('@Model.Provider.Id');">Delete provider</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -199,12 +193,12 @@
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content rounded">
|
||||
<div class="modal-body">
|
||||
<h4 class="font-weight-bolder">Cannot Delete @Model.Name</h4>
|
||||
<p class="font-weight-lighter">You must unlink all clients before you can delete @Model.Name.</p>
|
||||
<h4 class="fw-bolder">Cannot Delete @Model.Name</h4>
|
||||
<p class="fw-lighter">You must unlink all clients before you can delete @Model.Name.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary btn-pill" data-dismiss="modal">Ok</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-pill" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary btn-pill" data-bs-dismiss="modal">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -214,15 +208,14 @@
|
||||
|
||||
<div class="d-flex mt-4">
|
||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
||||
<div class="ml-auto d-flex">
|
||||
<div class="ms-auto d-flex">
|
||||
<button class="btn btn-danger" onclick="openRequestDeleteModal(@Model.ProviderOrganizations.Count())">Request Delete</button>
|
||||
<button id="requestDeletionBtn" hidden="hidden" data-toggle="modal" data-target="#requestDeletionModal"></button>
|
||||
<button id="requestDeletionBtn" hidden="hidden" data-bs-toggle="modal" data-bs-target="#requestDeletionModal"></button>
|
||||
|
||||
<button class="btn btn-outline-danger ml-2" onclick="openDeleteModal(@Model.ProviderOrganizations.Count())">Delete</button>
|
||||
<button id="deleteBtn" hidden="hidden" data-toggle="modal" data-target="#DeleteModal"></button>
|
||||
|
||||
<button id="linkAccWarningBtn" hidden="hidden" data-toggle="modal" data-target="#linkedWarningModal"></button>
|
||||
<button class="btn btn-outline-danger ms-2" onclick="openDeleteModal(@Model.ProviderOrganizations.Count())">Delete</button>
|
||||
<button id="deleteBtn" hidden="hidden" data-bs-toggle="modal" data-bs-target="#DeleteModal"></button>
|
||||
|
||||
<button id="linkAccWarningBtn" hidden="hidden" data-bs-toggle="modal" data-bs-target="#linkedWarningModal"></button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
@ -11,23 +11,27 @@
|
||||
|
||||
<h1>Providers</h1>
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col">
|
||||
<form class="form-inline mb-2" method="get">
|
||||
<label class="sr-only" asp-for="Name">Name</label>
|
||||
<input type="text" class="form-control mb-2 mr-2" placeholder="Name" asp-for="Name" name="name">
|
||||
<label class="sr-only" asp-for="UserEmail">User email</label>
|
||||
<input type="text" class="form-control mb-2 mr-2" placeholder="User email" asp-for="UserEmail" name="userEmail">
|
||||
<button type="submit" class="btn btn-primary mb-2" title="Search"><i class="fa fa-search"></i> Search</button>
|
||||
</form>
|
||||
<form class="row row-cols-lg-auto g-3 align-items-center mb-2" method="get">
|
||||
<div class="col-12">
|
||||
<label class="visually-hidden" asp-for="Name">Name</label>
|
||||
<input type="text" class="form-control" placeholder="Name" asp-for="Name" name="name">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="visually-hidden" asp-for="UserEmail">User email</label>
|
||||
<input type="text" class="form-control" placeholder="User email" asp-for="UserEmail" name="userEmail">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary" title="Search">
|
||||
<i class="fa fa-search"></i> Search
|
||||
</button>
|
||||
</div>
|
||||
@if (canCreateProvider)
|
||||
{
|
||||
<div class="col-auto">
|
||||
<div class="col-auto ms-auto">
|
||||
<a asp-action="Create" class="btn btn-secondary">Create Provider</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
|
@ -24,9 +24,9 @@
|
||||
<th>
|
||||
@if (Model.Provider.Type == ProviderType.Reseller)
|
||||
{
|
||||
<div class="float-right text-nowrap">
|
||||
<a asp-controller="Providers" asp-action="CreateOrganization" asp-route-providerId="@Model.Provider.Id" class="btn btn-sm btn-primary">New Organization</a>
|
||||
<a asp-controller="Providers" asp-action="AddExistingOrganization" asp-route-id="@Model.Provider.Id" class="btn btn-sm btn-outline-primary">Add Existing Organization</a>
|
||||
<div class="float-end text-nowrap">
|
||||
<a asp-controller="Providers" asp-action="CreateOrganization" asp-route-providerId="@Model.Provider.Id" class="btn btn-sm btn-primary text-decoration-none">New Organization</a>
|
||||
<a asp-controller="Providers" asp-action="AddExistingOrganization" asp-route-id="@Model.Provider.Id" class="btn btn-sm btn-outline-primary text-decoration-none">Add Existing Organization</a>
|
||||
</div>
|
||||
}
|
||||
</th>
|
||||
@ -51,16 +51,16 @@
|
||||
@providerOrganization.Status
|
||||
</td>
|
||||
<td>
|
||||
<div class="float-right">
|
||||
<div class="float-end">
|
||||
@if (canUnlinkFromProvider)
|
||||
{
|
||||
<a href="#" class="text-danger float-right" onclick="return unlinkProvider('@Model.Provider.Id', '@providerOrganization.Id');">
|
||||
<a href="#" class="text-danger float-end" onclick="return unlinkProvider('@Model.Provider.Id', '@providerOrganization.Id');">
|
||||
Unlink provider
|
||||
</a>
|
||||
}
|
||||
@if (providerOrganization.Status == OrganizationStatusType.Pending)
|
||||
{
|
||||
<a href="#" class="float-right mr-3" onclick="return resendOwnerInvite('@providerOrganization.OrganizationId');">
|
||||
<a href="#" class="float-end me-3" onclick="return resendOwnerInvite('@providerOrganization.OrganizationId');">
|
||||
Resend invitation
|
||||
</a>
|
||||
}
|
||||
|
@ -26,8 +26,8 @@
|
||||
<h2>General</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="Name"></label>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="Name"></label>
|
||||
<input type="text" class="form-control" asp-for="Name" value="@Model.Name" required>
|
||||
</div>
|
||||
</div>
|
||||
@ -37,17 +37,17 @@
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label>Client Owner Email</label>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Client Owner Email</label>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Owners))
|
||||
{
|
||||
<input type="text" class="form-control" asp-for="Owners" readonly="readonly">
|
||||
<input type="text" class="form-control" asp-for="Owners" readonly>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="text" class="form-control" asp-for="Owners" required>
|
||||
}
|
||||
<label class="form-check-label small text-muted align-top">This user should be independent of the Provider. If the Provider is disassociated with the organization, this user will maintain ownership of the organization.</label>
|
||||
<div class="form-text mt-0">This user should be independent of the Provider. If the Provider is disassociated with the organization, this user will maintain ownership of the organization.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -66,8 +66,8 @@
|
||||
<h2>Plan</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="PlanType"></label>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="PlanType"></label>
|
||||
@{
|
||||
var planTypes = Enum.GetValues<PlanType>()
|
||||
.Where(p =>
|
||||
@ -83,12 +83,12 @@
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
<select class="form-control" asp-for="PlanType" asp-items="planTypes" disabled='@(canEditPlan ? null : "disabled")'></select>
|
||||
<select class="form-select" asp-for="PlanType" asp-items="planTypes" disabled='@(canEditPlan ? null : "disabled")'></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="Plan"></label>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="Plan"></label>
|
||||
<input type="text" class="form-control" asp-for="Plan" required readonly='@(!canEditPlan)'>
|
||||
</div>
|
||||
</div>
|
||||
@ -172,28 +172,28 @@
|
||||
<h2>Password Manager Configuration</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="Seats"></label>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="Seats"></label>
|
||||
<input type="number" class="form-control" asp-for="Seats" min="1" readonly='@(!canEditPlan)'>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="MaxCollections"></label>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="MaxCollections"></label>
|
||||
<input type="number" class="form-control" asp-for="MaxCollections" min="1" readonly='@(!canEditPlan)'>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="MaxStorageGb"></label>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="MaxStorageGb"></label>
|
||||
<input type="number" class="form-control" asp-for="MaxStorageGb" min="1" readonly='@(!canEditPlan)'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<div class="form-group">
|
||||
<label asp-for="MaxAutoscaleSeats"></label>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="MaxAutoscaleSeats"></label>
|
||||
<input type="number" class="form-control" asp-for="MaxAutoscaleSeats" min="1" readonly='@(!canEditPlan)'>
|
||||
</div>
|
||||
</div>
|
||||
@ -202,32 +202,32 @@
|
||||
|
||||
@if (canViewPlan)
|
||||
{
|
||||
<div id="organization-secrets-configuration" hidden="@(!Model.UseSecretsManager)">
|
||||
<div id="organization-secrets-configuration" @(Model.UseSecretsManager ? null : "lass='d-none'")>
|
||||
<h2>Secrets Manager Configuration</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="SmSeats"></label>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="SmSeats"></label>
|
||||
<input type="number" class="form-control" asp-for="SmSeats" min="1" readonly='@(!canEditPlan)'>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="MaxAutoscaleSmSeats"></label>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="MaxAutoscaleSmSeats"></label>
|
||||
<input type="number" class="form-control" asp-for="MaxAutoscaleSmSeats" min="1" readonly='@(!canEditPlan)'>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="SmServiceAccounts"></label>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="SmServiceAccounts"></label>
|
||||
<input type="number" class="form-control" asp-for="SmServiceAccounts" min="1" readonly='@(!canEditPlan)'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<div class="form-group">
|
||||
<label asp-for="MaxAutoscaleSmServiceAccounts"></label>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="MaxAutoscaleSmServiceAccounts"></label>
|
||||
<input type="number" class="form-control" asp-for="MaxAutoscaleSmServiceAccounts" min="1" readonly='@(!canEditPlan)'>
|
||||
</div>
|
||||
</div>
|
||||
@ -240,14 +240,14 @@
|
||||
<h2>Licensing</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="LicenseKey"></label>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="LicenseKey"></label>
|
||||
<input type="text" class="form-control" asp-for="LicenseKey" readonly='@(!canEditLicensing)'>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="ExpirationDate"></label>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="ExpirationDate"></label>
|
||||
<input type="datetime-local" class="form-control" asp-for="ExpirationDate" readonly='@(!canEditLicensing)' step="1">
|
||||
</div>
|
||||
</div>
|
||||
@ -259,52 +259,46 @@
|
||||
<h2>Billing</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="BillingEmail"></label>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="BillingEmail"></label>
|
||||
<input type="email" class="form-control" asp-for="BillingEmail" readonly="readonly">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<div class="form-group">
|
||||
<label asp-for="Gateway"></label>
|
||||
<select class="form-control" asp-for="Gateway" disabled='@(canEditBilling ? null : "disabled")'
|
||||
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="Gateway"></label>
|
||||
<select class="form-select" asp-for="Gateway" disabled="@(!canEditBilling)"
|
||||
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="GatewayCustomerId"></label>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="GatewayCustomerId"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewayCustomerId" readonly='@(!canEditBilling)'>
|
||||
@if(canLaunchGateway)
|
||||
{
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-secondary" type="button" id="gateway-customer-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-secondary" type="button" id="gateway-customer-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="GatewaySubscriptionId"></label>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="GatewaySubscriptionId"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewaySubscriptionId" readonly='@(!canEditBilling)'>
|
||||
@if (canLaunchGateway)
|
||||
{
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,8 +3,8 @@
|
||||
ViewData["Title"] = "Login";
|
||||
}
|
||||
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="col col-lg-6 col-md-8">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6 col-md-8">
|
||||
@if(!string.IsNullOrWhiteSpace(Model.Success))
|
||||
{
|
||||
<div class="alert alert-success" role="alert">@Model.Success</div>
|
||||
@ -19,12 +19,12 @@
|
||||
<form asp-action="" method="post">
|
||||
<input type="hidden" asp-for="ReturnUrl" />
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Email" class="sr-only">Email Address</label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="Email" class="visually-hidden">Email Address</label>
|
||||
<input asp-for="Email" type="email" class="form-control" placeholder="ex. john@example.com"
|
||||
required autofocus>
|
||||
<span asp-validation-for="Email" class="invalid-feedback"></span>
|
||||
<small class="form-text text-muted">We'll email you a secure login link.</small>
|
||||
<div class="form-text">We'll email you a secure login link.</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Continue</button>
|
||||
</form>
|
||||
|
@ -25,22 +25,22 @@ bf82d3cf-0e21-4f39-b81b-ef52b2fc6a3a
|
||||
</div>
|
||||
<form method="post" asp-controller="MigrateProviders" asp-action="Post" class="mt-2">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ProviderIds"></label>
|
||||
<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="form-group">
|
||||
<input type="submit" value="Run" class="btn btn-primary mb-2"/>
|
||||
<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="form-group">
|
||||
<label asp-for="ProviderIds"></label>
|
||||
<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="form-group">
|
||||
<input type="submit" value="See Previous Results" class="btn btn-primary mb-2"/>
|
||||
<div class="mb-3">
|
||||
<input type="submit" value="See Previous Results" class="btn btn-primary"/>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
@ -1,4 +1,7 @@
|
||||
@import "webfonts.scss";
|
||||
@import "bootstrap/scss/functions";
|
||||
@import "bootstrap/scss/variables";
|
||||
@import "bootstrap/scss/mixins";
|
||||
|
||||
$primary: #175DDC;
|
||||
$primary-accent: #1252A3;
|
||||
@ -7,7 +10,8 @@ $info: #555555;
|
||||
$warning: #bf7e16;
|
||||
$danger: #dd4b39;
|
||||
|
||||
$theme-colors: ( "primary-accent": $primary-accent );
|
||||
$theme-colors: map-merge($theme-colors, ("primary-accent": $primary-accent));
|
||||
|
||||
$font-family-sans-serif: 'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
|
||||
$h1-font-size: 2rem;
|
||||
@ -17,7 +21,7 @@ $h4-font-size: 1rem;
|
||||
$h5-font-size: 1rem;
|
||||
$h6-font-size: 1rem;
|
||||
|
||||
@import "bootstrap/scss/bootstrap.scss";
|
||||
@import "bootstrap/scss/bootstrap";
|
||||
|
||||
h1 {
|
||||
border-bottom: 1px solid $border-color;
|
||||
@ -49,3 +53,11 @@ h3 {
|
||||
.form-check-input {
|
||||
margin-top: .45rem;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +105,7 @@
|
||||
<h3>SMTP</h3>
|
||||
@if(!Bit.Core.Utilities.CoreHelpers.SettingHasValue(Model.GlobalSettings.Mail?.Smtp?.Host))
|
||||
{
|
||||
<p class="text-muted">Not configured</p>
|
||||
<p class="text-body-secondary">Not configured</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -159,7 +159,7 @@ else
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Not configured</span>
|
||||
<span class="text-body-secondary">Not configured</span>
|
||||
}
|
||||
</dd>
|
||||
|
||||
@ -171,7 +171,7 @@ else
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Not configured</span>
|
||||
<span class="text-body-secondary">Not configured</span>
|
||||
}
|
||||
</dd>
|
||||
|
||||
@ -183,7 +183,7 @@ else
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Not configured</span>
|
||||
<span class="text-body-secondary">Not configured</span>
|
||||
}
|
||||
</dd>
|
||||
</dl>
|
||||
|
@ -37,12 +37,12 @@
|
||||
<a class="navbar-brand" asp-controller="Home" asp-action="Index">
|
||||
<i class="fa fa-lg fa-fw fa-shield"></i> Admin
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse"
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
|
||||
aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-md-0">
|
||||
@if (SignInManager.IsSignedIn(User))
|
||||
{
|
||||
@if (canViewUsers)
|
||||
@ -69,10 +69,10 @@
|
||||
{
|
||||
<li class="nav-item dropdown" active-controller="tools">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="toolsDropdown" role="button"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Tools
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="toolsDropdown">
|
||||
<ul class="dropdown-menu" aria-labelledby="toolsDropdown">
|
||||
@if (canChargeBraintree)
|
||||
{
|
||||
<a class="dropdown-item" asp-controller="Tools" asp-action="ChargeBraintree">
|
||||
@ -121,7 +121,7 @@
|
||||
Migrate Providers
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</ul>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
@ -10,30 +10,30 @@
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="UserId"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="UserId" class="form-label"></label>
|
||||
<input type="text" class="form-control" asp-for="UserId">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="OrganizationId"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="OrganizationId" class="form-label"></label>
|
||||
<input type="text" class="form-control" asp-for="OrganizationId">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="Date"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="Date" class="form-label"></label>
|
||||
<input type="datetime-local" class="form-control" asp-for="Date" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<div class="form-group">
|
||||
<label asp-for="Type"></label>
|
||||
<select class="form-control" asp-for="Type" required
|
||||
<div class="mb-3">
|
||||
<label asp-for="Type" class="form-label"></label>
|
||||
<select class="form-select" asp-for="Type" required
|
||||
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.TransactionType>()"></select>
|
||||
</div>
|
||||
</div>
|
||||
@ -41,24 +41,20 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="Amount"></label>
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">$</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label asp-for="Amount" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" min="-1000000.00" max="1000000.00" step="0.01" class="form-control"
|
||||
asp-for="Amount" required placeholder="ex. 10.00">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="RefundedAmount"></label>
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">$</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label asp-for="RefundedAmount" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" min="0.01" max="1000000.00" step="0.01" class="form-control"
|
||||
asp-for="RefundedAmount" placeholder="ex. 10.00">
|
||||
</div>
|
||||
@ -69,39 +65,35 @@
|
||||
<input type="checkbox" class="form-check-input" asp-for="Refunded">
|
||||
<label class="form-check-label" asp-for="Refunded"></label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Details"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="Details" class="form-label"></label>
|
||||
<input type="text" class="form-control" asp-for="Details" required>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<div class="form-group">
|
||||
<label asp-for="Gateway"></label>
|
||||
<select class="form-control" asp-for="Gateway"
|
||||
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label asp-for="Gateway" class="form-label"></label>
|
||||
<select class="form-select" asp-for="Gateway"
|
||||
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="GatewayId"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="GatewayId" class="form-label"></label>
|
||||
<input type="text" class="form-control" asp-for="GatewayId">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="form-group">
|
||||
<label asp-for="PaymentMethod"></label>
|
||||
<select class="form-control" asp-for="PaymentMethod"
|
||||
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.PaymentMethodType>()">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label asp-for="PaymentMethod" class="form-label"></label>
|
||||
<select class="form-select" asp-for="PaymentMethod"
|
||||
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.PaymentMethodType>()">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,28 +9,28 @@
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="UserId"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="UserId" class="form-label"></label>
|
||||
<input type="text" class="form-control" asp-for="UserId">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="OrganizationId"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="OrganizationId" class="form-label"></label>
|
||||
<input type="text" class="form-control" asp-for="OrganizationId">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="InstallationId"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="InstallationId" class="form-label"></label>
|
||||
<input type="text" class="form-control" asp-for="InstallationId">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="Version"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="Version" class="form-label"></label>
|
||||
<input type="number" class="form-control" asp-for="Version">
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,17 +9,17 @@
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="UserId"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="UserId" class="form-label"></label>
|
||||
<input type="text" class="form-control" asp-for="UserId">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="OrganizationId"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="OrganizationId" class="form-label"></label>
|
||||
<input type="text" class="form-control" asp-for="OrganizationId">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mb-2">Promote Admin</button>
|
||||
<button type="submit" class="btn btn-primary">Promote Admin</button>
|
||||
</form>
|
@ -74,208 +74,204 @@
|
||||
<div class="alert alert-success"></div>
|
||||
}
|
||||
<form method="post">
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<label asp-for="Filter.Status">Status</label>
|
||||
<select asp-for="Filter.Status" name="filter.Status" class="form-control mr-2">
|
||||
<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-6">
|
||||
<label asp-for="Filter.CurrentPeriodEnd">Current Period End</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-append">
|
||||
<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">
|
||||
<span class="mr-1">
|
||||
<input type="radio" class="mr-1" asp-for="Filter.CurrentPeriodEndRange" name="filter.CurrentPeriodEndRange" value="lt">Before
|
||||
</span>
|
||||
<input type="radio" asp-for="Filter.CurrentPeriodEndRange" name="filter.CurrentPeriodEndRange" value="gt">After
|
||||
<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>
|
||||
</div>
|
||||
@{
|
||||
var date = @Model.Filter.CurrentPeriodEndDate.HasValue ? @Model.Filter.CurrentPeriodEndDate.Value.ToString("yyyy-MM-dd") : string.Empty;
|
||||
@{
|
||||
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>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-6">
|
||||
<label asp-for="Filter.Price">Price ID</label>
|
||||
<select asp-for="Filter.Price" name="filter.Price" class="form-control mr-2">
|
||||
<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>
|
||||
<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="col-6">
|
||||
<label asp-for="Filter.TestClock">Test Clock</label>
|
||||
<select asp-for="Filter.TestClock" name="filter.TestClock" class="form-control mr-2">
|
||||
<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>
|
||||
<div class="row col-12 d-flex justify-content-end my-3">
|
||||
<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>
|
||||
<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-muted">✔ All subscriptions for this search are selected.</span><br/>
|
||||
<div class="alert alert-warning" 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">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="form-check form-check-inline">
|
||||
<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())
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<td colspan="6">No results to list.</td>
|
||||
<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>
|
||||
}
|
||||
else
|
||||
{
|
||||
@for (var i = 0; i < Model.Items.Count; i++)
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (!Model.Items.Any())
|
||||
{
|
||||
<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" 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>
|
||||
<td colspan="6">No results to list.</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<nav class="d-inline-flex">
|
||||
<ul class="pagination">
|
||||
@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"
|
||||
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.NextPage">
|
||||
Next
|
||||
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>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Next</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<span id="bulkActions" class="d-none ml-2">
|
||||
<span class="d-inline-flex">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary mr-1"
|
||||
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>
|
||||
<button type="submit" class="btn btn-danger" name="action" asp-for="Action" value="@StripeSubscriptionsAction.BulkCancel">
|
||||
Bulk Cancel
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</nav>
|
||||
</nav>
|
||||
</form>
|
||||
|
@ -27,20 +27,20 @@
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" asp-action="TaxRateUpload">
|
||||
<div class="form-group">
|
||||
<input type="file" name="file" />
|
||||
<div class="mb-3">
|
||||
<input type="file" class="form-control" name="file" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" value="Upload" class="btn btn-primary mb-2" />
|
||||
<div class="mb-3">
|
||||
<input type="submit" value="Upload" class="btn btn-primary" />
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<hr/>
|
||||
<hr class="my-4">
|
||||
<h2>View & Manage Tax Rates</h2>
|
||||
<a class="btn btn-primary mb-2" asp-controller="Tools" asp-action="TaxRateAddEdit">Add a Rate</a>
|
||||
<a class="btn btn-primary mb-3" asp-controller="Tools" asp-action="TaxRateAddEdit">Add a Rate</a>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<table class="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 190px;">Id</th>
|
||||
@ -97,7 +97,7 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<nav aria-label="Tax rates pagination">
|
||||
<ul class="pagination">
|
||||
@if(Model.PreviousPage.HasValue)
|
||||
{
|
||||
|
@ -115,8 +115,8 @@
|
||||
<h2>Premium</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="MaxStorageGb"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="MaxStorageGb" class="form-label"></label>
|
||||
<input type="number" class="form-control" asp-for="MaxStorageGb" min="1" readonly='@(!canEditPremium)'>
|
||||
</div>
|
||||
</div>
|
||||
@ -131,14 +131,14 @@
|
||||
<h2>Licensing</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="LicenseKey"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="LicenseKey" class="form-label"></label>
|
||||
<input type="text" class="form-control" asp-for="LicenseKey" readonly='@(!canEditLicensing)'>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="PremiumExpirationDate"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="PremiumExpirationDate" class="form-label"></label>
|
||||
<input type="datetime-local" class="form-control" asp-for="PremiumExpirationDate" readonly='@(!canEditLicensing)'>
|
||||
</div>
|
||||
</div>
|
||||
@ -149,44 +149,38 @@
|
||||
<h2>Billing</h2>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<div class="form-group">
|
||||
<label asp-for="Gateway"></label>
|
||||
<select class="form-control" asp-for="Gateway" disabled='@(canEditBilling ? null : "disabled")'
|
||||
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label asp-for="Gateway" class="form-label"></label>
|
||||
<select class="form-select" asp-for="Gateway" disabled='@(canEditBilling ? null : "disabled")'
|
||||
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="GatewayCustomerId"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="GatewayCustomerId" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewayCustomerId" readonly='@(!canEditBilling)'>
|
||||
@if (canLaunchGateway)
|
||||
{
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-secondary" type="button" id="gateway-customer-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-secondary" type="button" id="gateway-customer-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="GatewaySubscriptionId"></label>
|
||||
<div class="mb-3">
|
||||
<label asp-for="GatewaySubscriptionId" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewaySubscriptionId" readonly='@(!canEditBilling)'>
|
||||
@if (canLaunchGateway)
|
||||
{
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -196,10 +190,10 @@
|
||||
</form>
|
||||
<div class="d-flex mt-4">
|
||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
||||
<div class="ml-auto d-flex">
|
||||
<div class="ms-auto d-flex">
|
||||
@if (canUpgradePremium)
|
||||
{
|
||||
<button class="btn btn-secondary mr-2" type="button" id="upgrade-premium">
|
||||
<button class="btn btn-secondary me-2" type="button" id="upgrade-premium">
|
||||
Upgrade Premium
|
||||
</button>
|
||||
}
|
||||
|
@ -5,10 +5,16 @@
|
||||
|
||||
<h1>Users</h1>
|
||||
|
||||
<form class="form-inline mb-2" method="get">
|
||||
<label class="sr-only" asp-for="Email">Email</label>
|
||||
<input type="text" class="form-control mb-2 mr-2" placeholder="Email" asp-for="Email" name="email">
|
||||
<button type="submit" class="btn btn-primary mb-2" title="Search"><i class="fa fa-search"></i> Search</button>
|
||||
<form class="row row-cols-lg-auto g-3 align-items-center mb-2" method="get">
|
||||
<div class="col-12">
|
||||
<label class="visually-hidden" asp-for="Email">Email</label>
|
||||
<input type="text" class="form-control" placeholder="Email" asp-for="Email" name="email">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary" title="Search">
|
||||
<i class="fa fa-search"></i> Search
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
@ -49,7 +55,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fa fa-star-o fa-lg fa-fw text-muted" title="Not Premium"></i>
|
||||
<i class="fa fa-star-o fa-lg fa-fw text-body-secondary" title="Not Premium"></i>
|
||||
}
|
||||
@if (user.MaxStorageGb.HasValue && user.MaxStorageGb > 1)
|
||||
{
|
||||
@ -59,7 +65,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fa fa-plus-square-o fa-lg fa-fw text-muted"
|
||||
<i class="fa fa-plus-square-o fa-lg fa-fw text-body-secondary"
|
||||
title="No Additional Storage">
|
||||
</i>
|
||||
}
|
||||
@ -69,7 +75,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fa fa-times-circle-o fa-lg fa-fw text-muted" title="Email Not Verified"></i>
|
||||
<i class="fa fa-times-circle-o fa-lg fa-fw text-body-secondary" title="Email Not Verified"></i>
|
||||
}
|
||||
@if (user.TwoFactorEnabled)
|
||||
{
|
||||
@ -77,7 +83,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fa fa-unlock fa-lg fa-fw text-muted" title="2FA Not Enabled"></i>
|
||||
<i class="fa fa-unlock fa-lg fa-fw text-body-secondary" title="2FA Not Enabled"></i>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
|
35
src/Admin/package-lock.json
generated
35
src/Admin/package-lock.json
generated
@ -9,10 +9,9 @@
|
||||
"version": "0.0.0",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"bootstrap": "4.6.2",
|
||||
"bootstrap": "5.3.3",
|
||||
"font-awesome": "4.7.0",
|
||||
"jquery": "3.7.1",
|
||||
"popper.js": "1.16.1",
|
||||
"toastr": "2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -385,6 +384,17 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
@ -703,9 +713,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
|
||||
"integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==",
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
|
||||
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@ -716,10 +726,8 @@
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"jquery": "1.9.1 - 3",
|
||||
"popper.js": "^1.16.1"
|
||||
"@popperjs/core": "^2.11.8"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
@ -1578,17 +1586,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/popper.js": {
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
|
||||
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
|
||||
"deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.47",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||
|
@ -8,10 +8,9 @@
|
||||
"build": "webpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "4.6.2",
|
||||
"bootstrap": "5.3.3",
|
||||
"font-awesome": "4.7.0",
|
||||
"jquery": "3.7.1",
|
||||
"popper.js": "1.16.1",
|
||||
"toastr": "2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -13,8 +13,6 @@ module.exports = {
|
||||
entry: {
|
||||
site: [
|
||||
path.resolve(__dirname, paths.sassDir, "site.scss"),
|
||||
|
||||
"popper.js",
|
||||
"bootstrap",
|
||||
"jquery",
|
||||
"font-awesome/css/font-awesome.css",
|
||||
|
@ -2,15 +2,16 @@
|
||||
using Bit.Api.AdminConsole.Models.Response;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Groups;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -89,11 +90,34 @@ public class GroupsController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<GroupDetailsResponseModel>> Get(Guid orgId)
|
||||
public async Task<ListResponseModel<GroupDetailsResponseModel>> GetOrganizationGroups(Guid orgId)
|
||||
{
|
||||
var authorized =
|
||||
(await _authorizationService.AuthorizeAsync(User, GroupOperations.ReadAll(orgId))).Succeeded;
|
||||
if (!authorized)
|
||||
var authResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAll);
|
||||
if (!authResult.Succeeded)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.SecureOrgGroupDetails))
|
||||
{
|
||||
var groups = await _groupRepository.GetManyByOrganizationIdAsync(orgId);
|
||||
var responses = groups.Select(g => new GroupDetailsResponseModel(g, []));
|
||||
return new ListResponseModel<GroupDetailsResponseModel>(responses);
|
||||
}
|
||||
|
||||
var groupDetails = await _groupRepository.GetManyWithCollectionsByOrganizationIdAsync(orgId);
|
||||
var detailResponses = groupDetails.Select(g => new GroupDetailsResponseModel(g.Item1, g.Item2));
|
||||
return new ListResponseModel<GroupDetailsResponseModel>(detailResponses);
|
||||
}
|
||||
|
||||
[HttpGet("details")]
|
||||
public async Task<ListResponseModel<GroupDetailsResponseModel>> GetOrganizationGroupDetails(Guid orgId)
|
||||
{
|
||||
var authResult = _featureService.IsEnabled(FeatureFlagKeys.SecureOrgGroupDetails)
|
||||
? await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAllDetails)
|
||||
: await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAll);
|
||||
|
||||
if (!authResult.Succeeded)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
|
@ -289,9 +289,7 @@ public class OrganizationsController : Controller
|
||||
throw new BadRequestException(string.Empty, "User verification failed.");
|
||||
}
|
||||
|
||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
|
||||
if (consolidatedBillingEnabled && organization.IsValidClient())
|
||||
if (organization.IsValidClient())
|
||||
{
|
||||
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||
|
||||
@ -322,8 +320,7 @@ public class OrganizationsController : Controller
|
||||
throw new BadRequestException("Invalid token.");
|
||||
}
|
||||
|
||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
if (consolidatedBillingEnabled && organization.IsValidClient())
|
||||
if (organization.IsValidClient())
|
||||
{
|
||||
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||
if (provider.IsBillable())
|
||||
@ -357,7 +354,7 @@ public class OrganizationsController : Controller
|
||||
{
|
||||
// Non-enterprise orgs should not be able to create or view an apikey of billing sync/scim key types
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
if (plan.ProductTier != ProductTierType.Enterprise)
|
||||
if (plan.ProductTier is not ProductTierType.Enterprise and not ProductTierType.Teams)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
@ -213,7 +213,7 @@ public class MembersController : Controller
|
||||
{
|
||||
return new NotFoundResult();
|
||||
}
|
||||
await _updateOrganizationUserGroupsCommand.UpdateUserGroupsAsync(existingUser, model.GroupIds, null);
|
||||
await _updateOrganizationUserGroupsCommand.UpdateUserGroupsAsync(existingUser, model.GroupIds);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<UserSecretsId>bitwarden-Api</UserSecretsId>
|
||||
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
||||
|
@ -3,7 +3,7 @@ using Bit.Api.AdminConsole.Models.Response;
|
||||
using Bit.Api.Auth.Models.Request;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Api.Auth.Validators;
|
||||
using Bit.Api.KeyManagement.Validators;
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Request.Accounts;
|
||||
using Bit.Api.Models.Response;
|
||||
@ -18,7 +18,6 @@ using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Services;
|
||||
@ -26,6 +25,8 @@ using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Models.Api.Response;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
|
@ -4,15 +4,14 @@ using Bit.Api.Auth.Models.Response.TwoFactor;
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Utilities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Fido2NetLib;
|
||||
@ -29,11 +28,10 @@ public class TwoFactorController : Controller
|
||||
private readonly IUserService _userService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly UserManager<User> _userManager;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IVerifyAuthRequestCommand _verifyAuthRequestCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IDuoUniversalTokenService _duoUniversalTokenService;
|
||||
private readonly IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> _twoFactorAuthenticatorDataProtector;
|
||||
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmailTwoFactorSessionDataProtector;
|
||||
|
||||
@ -41,22 +39,20 @@ public class TwoFactorController : Controller
|
||||
IUserService userService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationService organizationService,
|
||||
GlobalSettings globalSettings,
|
||||
UserManager<User> userManager,
|
||||
ICurrentContext currentContext,
|
||||
IVerifyAuthRequestCommand verifyAuthRequestCommand,
|
||||
IFeatureService featureService,
|
||||
IDuoUniversalTokenService duoUniversalConfigService,
|
||||
IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> twoFactorAuthenticatorDataProtector,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmailTwoFactorSessionDataProtector)
|
||||
{
|
||||
_userService = userService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationService = organizationService;
|
||||
_globalSettings = globalSettings;
|
||||
_userManager = userManager;
|
||||
_currentContext = currentContext;
|
||||
_verifyAuthRequestCommand = verifyAuthRequestCommand;
|
||||
_featureService = featureService;
|
||||
_duoUniversalTokenService = duoUniversalConfigService;
|
||||
_twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector;
|
||||
_ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector;
|
||||
}
|
||||
@ -184,21 +180,7 @@ public class TwoFactorController : Controller
|
||||
public async Task<TwoFactorDuoResponseModel> PutDuo([FromBody] UpdateTwoFactorDuoRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model, true);
|
||||
try
|
||||
{
|
||||
// for backwards compatibility - will be removed with PM-8107
|
||||
DuoApi duoApi = null;
|
||||
if (model.ClientId != null && model.ClientSecret != null)
|
||||
{
|
||||
duoApi = new DuoApi(model.ClientId, model.ClientSecret, model.Host);
|
||||
}
|
||||
else
|
||||
{
|
||||
duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host);
|
||||
}
|
||||
await duoApi.JSONApiCall("GET", "/auth/v2/check");
|
||||
}
|
||||
catch (DuoException)
|
||||
if (!await _duoUniversalTokenService.ValidateDuoConfiguration(model.ClientSecret, model.ClientId, model.Host))
|
||||
{
|
||||
throw new BadRequestException(
|
||||
"Duo configuration settings are not valid. Please re-check the Duo Admin panel.");
|
||||
@ -241,21 +223,7 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid) ?? throw new NotFoundException();
|
||||
try
|
||||
{
|
||||
// for backwards compatibility - will be removed with PM-8107
|
||||
DuoApi duoApi = null;
|
||||
if (model.ClientId != null && model.ClientSecret != null)
|
||||
{
|
||||
duoApi = new DuoApi(model.ClientId, model.ClientSecret, model.Host);
|
||||
}
|
||||
else
|
||||
{
|
||||
duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host);
|
||||
}
|
||||
await duoApi.JSONApiCall("GET", "/auth/v2/check");
|
||||
}
|
||||
catch (DuoException)
|
||||
if (!await _duoUniversalTokenService.ValidateDuoConfiguration(model.ClientSecret, model.ClientId, model.Host))
|
||||
{
|
||||
throw new BadRequestException(
|
||||
"Duo configuration settings are not valid. Please re-check the Duo Admin panel.");
|
||||
|
@ -2,8 +2,8 @@
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Utilities;
|
||||
using Bit.Core.Entities;
|
||||
using Fido2NetLib;
|
||||
|
||||
@ -43,21 +43,16 @@ public class UpdateTwoFactorAuthenticatorRequestModel : SecretVerificationReques
|
||||
public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IValidatableObject
|
||||
{
|
||||
/*
|
||||
To support both v2 and v4 we need to remove the required annotation from the properties.
|
||||
todo - the required annotation will be added back in PM-8107.
|
||||
String lengths based on Duo's documentation
|
||||
https://github.com/duosecurity/duo_universal_csharp/blob/main/DuoUniversal/Client.cs
|
||||
*/
|
||||
[StringLength(50)]
|
||||
public string ClientId { get; set; }
|
||||
[StringLength(50)]
|
||||
public string ClientSecret { get; set; }
|
||||
//todo - will remove SKey and IKey with PM-8107
|
||||
[StringLength(50)]
|
||||
public string IntegrationKey { get; set; }
|
||||
//todo - will remove SKey and IKey with PM-8107
|
||||
[StringLength(50)]
|
||||
public string SecretKey { get; set; }
|
||||
[Required]
|
||||
[StringLength(50)]
|
||||
[StringLength(20, MinimumLength = 20, ErrorMessage = "Client Id must be exactly 20 characters.")]
|
||||
public string ClientId { get; set; }
|
||||
[Required]
|
||||
[StringLength(40, MinimumLength = 40, ErrorMessage = "Client Secret must be exactly 40 characters.")]
|
||||
public string ClientSecret { get; set; }
|
||||
[Required]
|
||||
public string Host { get; set; }
|
||||
|
||||
public User ToUser(User existingUser)
|
||||
@ -65,22 +60,17 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
|
||||
var providers = existingUser.GetTwoFactorProviders();
|
||||
if (providers == null)
|
||||
{
|
||||
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
||||
providers = [];
|
||||
}
|
||||
else if (providers.ContainsKey(TwoFactorProviderType.Duo))
|
||||
{
|
||||
providers.Remove(TwoFactorProviderType.Duo);
|
||||
}
|
||||
|
||||
Temporary_SyncDuoParams();
|
||||
|
||||
providers.Add(TwoFactorProviderType.Duo, new TwoFactorProvider
|
||||
{
|
||||
MetaData = new Dictionary<string, object>
|
||||
{
|
||||
//todo - will remove SKey and IKey with PM-8107
|
||||
["SKey"] = SecretKey,
|
||||
["IKey"] = IntegrationKey,
|
||||
["ClientSecret"] = ClientSecret,
|
||||
["ClientId"] = ClientId,
|
||||
["Host"] = Host
|
||||
@ -96,22 +86,17 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
|
||||
var providers = existingOrg.GetTwoFactorProviders();
|
||||
if (providers == null)
|
||||
{
|
||||
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
||||
providers = [];
|
||||
}
|
||||
else if (providers.ContainsKey(TwoFactorProviderType.OrganizationDuo))
|
||||
{
|
||||
providers.Remove(TwoFactorProviderType.OrganizationDuo);
|
||||
}
|
||||
|
||||
Temporary_SyncDuoParams();
|
||||
|
||||
providers.Add(TwoFactorProviderType.OrganizationDuo, new TwoFactorProvider
|
||||
{
|
||||
MetaData = new Dictionary<string, object>
|
||||
{
|
||||
//todo - will remove SKey and IKey with PM-8107
|
||||
["SKey"] = SecretKey,
|
||||
["IKey"] = IntegrationKey,
|
||||
["ClientSecret"] = ClientSecret,
|
||||
["ClientId"] = ClientId,
|
||||
["Host"] = Host
|
||||
@ -124,34 +109,22 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
|
||||
|
||||
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (!DuoApi.ValidHost(Host))
|
||||
var results = new List<ValidationResult>();
|
||||
if (string.IsNullOrWhiteSpace(ClientId))
|
||||
{
|
||||
yield return new ValidationResult("Host is invalid.", [nameof(Host)]);
|
||||
results.Add(new ValidationResult("ClientId is required.", [nameof(ClientId)]));
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(ClientSecret) && string.IsNullOrWhiteSpace(ClientId) &&
|
||||
string.IsNullOrWhiteSpace(SecretKey) && string.IsNullOrWhiteSpace(IntegrationKey))
|
||||
{
|
||||
yield return new ValidationResult("Neither v2 or v4 values are valid.", [nameof(IntegrationKey), nameof(SecretKey), nameof(ClientSecret), nameof(ClientId)]);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
use this method to ensure that both v2 params and v4 params are in sync
|
||||
todo will be removed in pm-8107
|
||||
*/
|
||||
private void Temporary_SyncDuoParams()
|
||||
{
|
||||
// Even if IKey and SKey exist prioritize v4 params ClientId and ClientSecret
|
||||
if (!string.IsNullOrWhiteSpace(ClientSecret) && !string.IsNullOrWhiteSpace(ClientId))
|
||||
if (string.IsNullOrWhiteSpace(ClientSecret))
|
||||
{
|
||||
SecretKey = ClientSecret;
|
||||
IntegrationKey = ClientId;
|
||||
results.Add(new ValidationResult("ClientSecret is required.", [nameof(ClientSecret)]));
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(SecretKey) && !string.IsNullOrWhiteSpace(IntegrationKey))
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Host) || !DuoUniversalTokenService.ValidDuoHost(Host))
|
||||
{
|
||||
ClientSecret = SecretKey;
|
||||
ClientId = IntegrationKey;
|
||||
results.Add(new ValidationResult("Host is invalid.", [nameof(Host)]));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,37 +13,26 @@ public class TwoFactorDuoResponseModel : ResponseModel
|
||||
public TwoFactorDuoResponseModel(User user)
|
||||
: base(ResponseObj)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
|
||||
Build(provider);
|
||||
}
|
||||
|
||||
public TwoFactorDuoResponseModel(Organization org)
|
||||
public TwoFactorDuoResponseModel(Organization organization)
|
||||
: base(ResponseObj)
|
||||
{
|
||||
if (org == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(org));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(organization);
|
||||
|
||||
var provider = org.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
|
||||
var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
|
||||
Build(provider);
|
||||
}
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
public string Host { get; set; }
|
||||
//TODO - will remove SecretKey with PM-8107
|
||||
public string SecretKey { get; set; }
|
||||
//TODO - will remove IntegrationKey with PM-8107
|
||||
public string IntegrationKey { get; set; }
|
||||
public string ClientSecret { get; set; }
|
||||
public string ClientId { get; set; }
|
||||
|
||||
// updated build to assist in the EDD migration for the Duo 2FA provider
|
||||
private void Build(TwoFactorProvider provider)
|
||||
{
|
||||
if (provider?.MetaData != null && provider.MetaData.Count > 0)
|
||||
@ -54,36 +43,13 @@ public class TwoFactorDuoResponseModel : ResponseModel
|
||||
{
|
||||
Host = (string)host;
|
||||
}
|
||||
|
||||
//todo - will remove SKey and IKey with PM-8107
|
||||
// check Skey and IKey first if they exist
|
||||
if (provider.MetaData.TryGetValue("SKey", out var sKey))
|
||||
{
|
||||
ClientSecret = MaskKey((string)sKey);
|
||||
SecretKey = MaskKey((string)sKey);
|
||||
}
|
||||
if (provider.MetaData.TryGetValue("IKey", out var iKey))
|
||||
{
|
||||
IntegrationKey = (string)iKey;
|
||||
ClientId = (string)iKey;
|
||||
}
|
||||
|
||||
// Even if IKey and SKey exist prioritize v4 params ClientId and ClientSecret
|
||||
if (provider.MetaData.TryGetValue("ClientSecret", out var clientSecret))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace((string)clientSecret))
|
||||
{
|
||||
ClientSecret = MaskKey((string)clientSecret);
|
||||
SecretKey = MaskKey((string)clientSecret);
|
||||
}
|
||||
ClientSecret = MaskSecret((string)clientSecret);
|
||||
}
|
||||
if (provider.MetaData.TryGetValue("ClientId", out var clientId))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace((string)clientId))
|
||||
{
|
||||
ClientId = (string)clientId;
|
||||
IntegrationKey = (string)clientId;
|
||||
}
|
||||
ClientId = (string)clientId;
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -92,30 +58,7 @@ public class TwoFactorDuoResponseModel : ResponseModel
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
use this method to ensure that both v2 params and v4 params are in sync
|
||||
todo will be removed in pm-8107
|
||||
*/
|
||||
private void Temporary_SyncDuoParams()
|
||||
{
|
||||
// Even if IKey and SKey exist prioritize v4 params ClientId and ClientSecret
|
||||
if (!string.IsNullOrWhiteSpace(ClientSecret) && !string.IsNullOrWhiteSpace(ClientId))
|
||||
{
|
||||
SecretKey = ClientSecret;
|
||||
IntegrationKey = ClientId;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(SecretKey) && !string.IsNullOrWhiteSpace(IntegrationKey))
|
||||
{
|
||||
ClientSecret = SecretKey;
|
||||
ClientId = IntegrationKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidDataException("Invalid Duo parameters.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string MaskKey(string key)
|
||||
private static string MaskSecret(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key) || key.Length <= 6)
|
||||
{
|
||||
|
@ -22,9 +22,9 @@ public abstract class BaseBillingController : Controller
|
||||
new ErrorResponseModel(message),
|
||||
statusCode: StatusCodes.Status500InternalServerError);
|
||||
|
||||
public static JsonHttpResult<ErrorResponseModel> Unauthorized() =>
|
||||
public static JsonHttpResult<ErrorResponseModel> Unauthorized(string message = "Unauthorized.") =>
|
||||
TypedResults.Json(
|
||||
new ErrorResponseModel("Unauthorized."),
|
||||
new ErrorResponseModel(message),
|
||||
statusCode: StatusCodes.Status401Unauthorized);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Context;
|
||||
@ -9,7 +8,6 @@ namespace Bit.Api.Billing.Controllers;
|
||||
|
||||
public abstract class BaseProviderController(
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
ILogger<BaseProviderController> logger,
|
||||
IProviderRepository providerRepository,
|
||||
IUserService userService) : BaseBillingController
|
||||
@ -26,15 +24,6 @@ public abstract class BaseProviderController(
|
||||
Guid providerId,
|
||||
Func<Guid, bool> checkAuthorization)
|
||||
{
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
||||
{
|
||||
logger.LogError(
|
||||
"Cannot run Consolidated Billing operation for provider ({ProviderID}) while feature flag is disabled",
|
||||
providerId);
|
||||
|
||||
return (null, Error.NotFound());
|
||||
}
|
||||
|
||||
var provider = await providerRepository.GetByIdAsync(providerId);
|
||||
|
||||
if (provider == null)
|
||||
|
@ -206,6 +206,11 @@ public class OrganizationBillingController(
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
if (requestBody.DescriptorCode.Length != 6 || !requestBody.DescriptorCode.StartsWith("SM"))
|
||||
{
|
||||
return Error.BadRequest("Statement descriptor should be a 6-character value that starts with 'SM'");
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization == null)
|
||||
@ -213,7 +218,7 @@ public class OrganizationBillingController(
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
await subscriberService.VerifyBankAccount(organization, (requestBody.Amount1, requestBody.Amount2));
|
||||
await subscriberService.VerifyBankAccount(organization, requestBody.DescriptorCode);
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Response.Organizations;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -31,6 +34,8 @@ public class OrganizationSponsorshipsController : Controller
|
||||
private readonly ICloudSyncSponsorshipsCommand _syncSponsorshipsCommand;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public OrganizationSponsorshipsController(
|
||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
||||
@ -45,7 +50,9 @@ public class OrganizationSponsorshipsController : Controller
|
||||
IRemoveSponsorshipCommand removeSponsorshipCommand,
|
||||
ICloudSyncSponsorshipsCommand syncSponsorshipsCommand,
|
||||
IUserService userService,
|
||||
ICurrentContext currentContext)
|
||||
ICurrentContext currentContext,
|
||||
IPolicyRepository policyRepository,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_organizationSponsorshipRepository = organizationSponsorshipRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
@ -60,6 +67,8 @@ public class OrganizationSponsorshipsController : Controller
|
||||
_syncSponsorshipsCommand = syncSponsorshipsCommand;
|
||||
_userService = userService;
|
||||
_currentContext = currentContext;
|
||||
_policyRepository = policyRepository;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
[Authorize("Application")]
|
||||
@ -94,9 +103,20 @@ public class OrganizationSponsorshipsController : Controller
|
||||
[Authorize("Application")]
|
||||
[HttpPost("validate-token")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<bool> PreValidateSponsorshipToken([FromQuery] string sponsorshipToken)
|
||||
public async Task<PreValidateSponsorshipResponseModel> PreValidateSponsorshipToken([FromQuery] string sponsorshipToken)
|
||||
{
|
||||
return (await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email)).valid;
|
||||
var isFreeFamilyPolicyEnabled = false;
|
||||
var (isValid, sponsorship) = await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email);
|
||||
if (isValid && _featureService.IsEnabled(FeatureFlagKeys.DisableFreeFamiliesSponsorship) && sponsorship.SponsoringOrganizationId.HasValue)
|
||||
{
|
||||
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsorship.SponsoringOrganizationId.Value,
|
||||
PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||
isFreeFamilyPolicyEnabled = policy?.Enabled ?? false;
|
||||
}
|
||||
|
||||
var response = PreValidateSponsorshipResponseModel.From(isValid, isFreeFamilyPolicyEnabled);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
[Authorize("Application")]
|
||||
|
@ -19,14 +19,13 @@ namespace Bit.Api.Billing.Controllers;
|
||||
[Authorize("Application")]
|
||||
public class ProviderBillingController(
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
ILogger<BaseProviderController> logger,
|
||||
IProviderBillingService providerBillingService,
|
||||
IProviderPlanRepository providerPlanRepository,
|
||||
IProviderRepository providerRepository,
|
||||
ISubscriberService subscriberService,
|
||||
IStripeAdapter stripeAdapter,
|
||||
IUserService userService) : BaseProviderController(currentContext, featureService, logger, providerRepository, userService)
|
||||
IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService)
|
||||
{
|
||||
[HttpGet("invoices")]
|
||||
public async Task<IResult> GetInvoicesAsync([FromRoute] Guid providerId)
|
||||
|
@ -14,14 +14,13 @@ namespace Bit.Api.Billing.Controllers;
|
||||
[Route("providers/{providerId:guid}/clients")]
|
||||
public class ProviderClientsController(
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
ILogger<BaseProviderController> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IProviderBillingService providerBillingService,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IProviderRepository providerRepository,
|
||||
IProviderService providerService,
|
||||
IUserService userService) : BaseProviderController(currentContext, featureService, logger, providerRepository, userService)
|
||||
IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService)
|
||||
{
|
||||
[HttpPost]
|
||||
public async Task<IResult> CreateAsync(
|
||||
@ -102,15 +101,27 @@ public class ProviderClientsController(
|
||||
|
||||
var clientOrganization = await organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);
|
||||
|
||||
if (clientOrganization.Seats != requestBody.AssignedSeats)
|
||||
if (clientOrganization is not { Status: OrganizationStatusType.Managed })
|
||||
{
|
||||
await providerBillingService.AssignSeatsToClientOrganization(
|
||||
provider,
|
||||
clientOrganization,
|
||||
requestBody.AssignedSeats);
|
||||
return Error.ServerError();
|
||||
}
|
||||
|
||||
var seatAdjustment = requestBody.AssignedSeats - (clientOrganization.Seats ?? 0);
|
||||
|
||||
var seatAdjustmentResultsInPurchase = await providerBillingService.SeatAdjustmentResultsInPurchase(
|
||||
provider,
|
||||
clientOrganization.PlanType,
|
||||
seatAdjustment);
|
||||
|
||||
if (seatAdjustmentResultsInPurchase && !currentContext.ProviderProviderAdmin(provider.Id))
|
||||
{
|
||||
return Error.Unauthorized("Service users cannot purchase additional seats.");
|
||||
}
|
||||
|
||||
await providerBillingService.ScaleSeats(provider, clientOrganization.PlanType, seatAdjustment);
|
||||
|
||||
clientOrganization.Name = requestBody.Name;
|
||||
clientOrganization.Seats = requestBody.AssignedSeats;
|
||||
|
||||
await organizationRepository.ReplaceAsync(clientOrganization);
|
||||
|
||||
|
@ -4,8 +4,6 @@ namespace Bit.Api.Billing.Models.Requests;
|
||||
|
||||
public class VerifyBankAccountRequestBody
|
||||
{
|
||||
[Range(0, 99)]
|
||||
public long Amount1 { get; set; }
|
||||
[Range(0, 99)]
|
||||
public long Amount2 { get; set; }
|
||||
[Required]
|
||||
public string DescriptorCode { get; set; }
|
||||
}
|
||||
|
@ -6,12 +6,14 @@ public record OrganizationMetadataResponse(
|
||||
bool IsEligibleForSelfHost,
|
||||
bool IsManaged,
|
||||
bool IsOnSecretsManagerStandalone,
|
||||
bool IsSubscriptionUnpaid)
|
||||
bool IsSubscriptionUnpaid,
|
||||
bool HasSubscription)
|
||||
{
|
||||
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
|
||||
=> new(
|
||||
metadata.IsEligibleForSelfHost,
|
||||
metadata.IsManaged,
|
||||
metadata.IsOnSecretsManagerStandalone,
|
||||
metadata.IsSubscriptionUnpaid);
|
||||
metadata.IsSubscriptionUnpaid,
|
||||
metadata.HasSubscription);
|
||||
}
|
||||
|
@ -1,11 +1,10 @@
|
||||
using Bit.Api.Auth.Validators;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
|
||||
namespace Bit.Api.Vault.Validators;
|
||||
namespace Bit.Api.KeyManagement.Validators;
|
||||
|
||||
public class CipherRotationValidator : IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>
|
||||
{
|
@ -5,7 +5,7 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Api.Auth.Validators;
|
||||
namespace Bit.Api.KeyManagement.Validators;
|
||||
|
||||
public class EmergencyAccessRotationValidator : IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>,
|
||||
IEnumerable<EmergencyAccess>>
|
@ -1,11 +1,10 @@
|
||||
using Bit.Api.Auth.Validators;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
|
||||
namespace Bit.Api.Vault.Validators;
|
||||
namespace Bit.Api.KeyManagement.Validators;
|
||||
|
||||
public class FolderRotationValidator : IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>
|
||||
{
|
@ -1,7 +1,7 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
namespace Bit.Api.Auth.Validators;
|
||||
namespace Bit.Api.KeyManagement.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// A consistent interface for domains to validate re-encrypted data before saved to database. Some examples are:<br/>
|
@ -1,10 +1,9 @@
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Auth.Validators;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Validators;
|
||||
namespace Bit.Api.KeyManagement.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Organization user implementation for <see cref="IRotationValidator{T,R}"/>
|
@ -1,12 +1,11 @@
|
||||
using Bit.Api.Auth.Validators;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.Services;
|
||||
|
||||
namespace Bit.Api.Tools.Validators;
|
||||
namespace Bit.Api.KeyManagement.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Send implementation for <see cref="IRotationValidator{T,R}"/>
|
@ -4,7 +4,7 @@ using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
namespace Bit.Api.Auth.Validators;
|
||||
namespace Bit.Api.KeyManagement.Validators;
|
||||
|
||||
public class WebAuthnLoginKeyRotationValidator : IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
|
||||
{
|
@ -8,13 +8,10 @@ using Bit.Core.Utilities;
|
||||
using IdentityModel;
|
||||
using System.Globalization;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Validators;
|
||||
using Bit.Api.Auth.Models.Request;
|
||||
using Bit.Api.Auth.Validators;
|
||||
using Bit.Api.KeyManagement.Validators;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Tools.Validators;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Api.Vault.Validators;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.SharedWeb.Health;
|
||||
@ -23,7 +20,6 @@ using Microsoft.OpenApi.Models;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.UserFeatures;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
@ -32,10 +28,10 @@ using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Tools.ReportFeatures;
|
||||
|
||||
|
||||
|
||||
#if !OSS
|
||||
using Bit.Commercial.Core.SecretsManager;
|
||||
using Bit.Commercial.Core.Utilities;
|
||||
|
@ -7,7 +7,6 @@ using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.ReportFeatures.Interfaces;
|
||||
using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
|
||||
using Bit.Core.Tools.ReportFeatures.Requests;
|
||||
using Bit.Core.Tools.Requests;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -21,18 +20,21 @@ public class ReportsController : Controller
|
||||
private readonly IMemberAccessCipherDetailsQuery _memberAccessCipherDetailsQuery;
|
||||
private readonly IAddPasswordHealthReportApplicationCommand _addPwdHealthReportAppCommand;
|
||||
private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery;
|
||||
private readonly IDropPasswordHealthReportApplicationCommand _dropPwdHealthReportAppCommand;
|
||||
|
||||
public ReportsController(
|
||||
ICurrentContext currentContext,
|
||||
IMemberAccessCipherDetailsQuery memberAccessCipherDetailsQuery,
|
||||
IAddPasswordHealthReportApplicationCommand addPasswordHealthReportApplicationCommand,
|
||||
IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery
|
||||
IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery,
|
||||
IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand
|
||||
)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_memberAccessCipherDetailsQuery = memberAccessCipherDetailsQuery;
|
||||
_addPwdHealthReportAppCommand = addPasswordHealthReportApplicationCommand;
|
||||
_getPwdHealthReportAppQuery = getPasswordHealthReportApplicationQuery;
|
||||
_dropPwdHealthReportAppCommand = dropPwdHealthReportAppCommand;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -161,4 +163,26 @@ public class ReportsController : Controller
|
||||
|
||||
return await _addPwdHealthReportAppCommand.AddPasswordHealthReportApplicationAsync(commandRequests);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops a record from PasswordHealthReportApplication
|
||||
/// </summary>
|
||||
/// <param name="request">
|
||||
/// A single instance of DropPasswordHealthReportApplicationRequest
|
||||
/// { OrganizationId, array of PasswordHealthReportApplicationIds }
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotFoundException">If user does not have access to the organization</exception>
|
||||
/// <exception cref="BadRequestException">If the organization does not have any records</exception>
|
||||
[HttpDelete("password-health-report-application")]
|
||||
public async Task DropPasswordHealthReportApplication(
|
||||
[FromBody] DropPasswordHealthReportApplicationRequest request)
|
||||
{
|
||||
if (!await _currentContext.AccessReports(request.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _dropPwdHealthReportAppCommand.DropPasswordHealthReportApplicationAsync(request);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Groups;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
|
@ -1,62 +0,0 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Context;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Api.Vault.AuthorizationHandlers.Groups;
|
||||
|
||||
/// <summary>
|
||||
/// Handles authorization logic for Group operations.
|
||||
/// This uses new logic implemented in the Flexible Collections initiative.
|
||||
/// </summary>
|
||||
public class GroupAuthorizationHandler : AuthorizationHandler<GroupOperationRequirement>
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
|
||||
public GroupAuthorizationHandler(ICurrentContext currentContext)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
}
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
GroupOperationRequirement requirement)
|
||||
{
|
||||
// Acting user is not authenticated, fail
|
||||
if (!_currentContext.UserId.HasValue)
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
if (requirement.OrganizationId == default)
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
var org = _currentContext.GetOrganization(requirement.OrganizationId);
|
||||
|
||||
switch (requirement)
|
||||
{
|
||||
case not null when requirement.Name == nameof(GroupOperations.ReadAll):
|
||||
await CanReadAllAsync(context, requirement, org);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanReadAllAsync(AuthorizationHandlerContext context, GroupOperationRequirement requirement,
|
||||
CurrentContextOrganization? org)
|
||||
{
|
||||
// All users of an organization can read all groups belonging to the organization for collection access management
|
||||
if (org is not null)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow provider users to read all groups if they are a provider for the target organization
|
||||
if (await _currentContext.ProviderUserForOrgAsync(requirement.OrganizationId))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||
|
||||
namespace Bit.Api.Vault.AuthorizationHandlers.Groups;
|
||||
|
||||
public class GroupOperationRequirement : OperationAuthorizationRequirement
|
||||
{
|
||||
public Guid OrganizationId { get; init; }
|
||||
|
||||
public GroupOperationRequirement(string name, Guid organizationId)
|
||||
{
|
||||
Name = name;
|
||||
OrganizationId = organizationId;
|
||||
}
|
||||
}
|
||||
|
||||
public static class GroupOperations
|
||||
{
|
||||
public static GroupOperationRequirement ReadAll(Guid organizationId)
|
||||
{
|
||||
return new GroupOperationRequirement(nameof(ReadAll), organizationId);
|
||||
}
|
||||
}
|
@ -112,7 +112,7 @@ public class SyncController : Controller
|
||||
|
||||
private ICollection<CipherDetails> FilterSSHKeys(ICollection<CipherDetails> ciphers)
|
||||
{
|
||||
if (_currentContext.ClientVersion >= _sshKeyCipherMinimumVersion)
|
||||
if (_currentContext.ClientVersion >= _sshKeyCipherMinimumVersion || _featureService.IsEnabled(FeatureFlagKeys.SSHVersionCheckQAOverride))
|
||||
{
|
||||
return ciphers;
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
<input asp-for="Email" type="email" class="form-control" placeholder="ex. john@example.com"
|
||||
required autofocus>
|
||||
<span asp-validation-for="Email" class="invalid-feedback"></span>
|
||||
<small class="form-text text-muted">We'll email you a secure login link.</small>
|
||||
<small class="form-text text-body-secondary">We'll email you a secure login link.</small>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block" type="submit">Continue</button>
|
||||
</form>
|
||||
|
@ -15,6 +15,7 @@ public enum PolicyType : byte
|
||||
DisablePersonalVaultExport = 10,
|
||||
ActivateAutofill = 11,
|
||||
AutomaticAppLogIn = 12,
|
||||
FreeFamiliesSponsorshipPolicy = 13
|
||||
}
|
||||
|
||||
public static class PolicyTypeExtensions
|
||||
@ -40,6 +41,7 @@ public static class PolicyTypeExtensions
|
||||
PolicyType.DisablePersonalVaultExport => "Remove individual vault export",
|
||||
PolicyType.ActivateAutofill => "Active auto-fill",
|
||||
PolicyType.AutomaticAppLogIn => "Automatically log in users for allowed applications",
|
||||
PolicyType.FreeFamiliesSponsorshipPolicy => "Remove Free Bitwarden Families sponsorship"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,43 @@
|
||||
#nullable enable
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
||||
|
||||
public class GroupAuthorizationHandler(ICurrentContext currentContext)
|
||||
: AuthorizationHandler<GroupOperationRequirement, OrganizationScope>
|
||||
{
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
GroupOperationRequirement requirement, OrganizationScope organizationScope)
|
||||
{
|
||||
var authorized = requirement switch
|
||||
{
|
||||
not null when requirement.Name == nameof(GroupOperations.ReadAll) =>
|
||||
await CanReadAllAsync(organizationScope),
|
||||
not null when requirement.Name == nameof(GroupOperations.ReadAllDetails) =>
|
||||
await CanViewGroupDetailsAsync(organizationScope),
|
||||
_ => false
|
||||
};
|
||||
|
||||
if (requirement is not null && authorized)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> CanReadAllAsync(OrganizationScope organizationScope) =>
|
||||
currentContext.GetOrganization(organizationScope) is not null
|
||||
|| await currentContext.ProviderUserForOrgAsync(organizationScope);
|
||||
|
||||
private async Task<bool> CanViewGroupDetailsAsync(OrganizationScope organizationScope) =>
|
||||
currentContext.GetOrganization(organizationScope) is
|
||||
{ Type: OrganizationUserType.Owner } or
|
||||
{ Type: OrganizationUserType.Admin } or
|
||||
{
|
||||
Permissions: { ManageGroups: true } or
|
||||
{ ManageUsers: true }
|
||||
} ||
|
||||
await currentContext.ProviderUserForOrgAsync(organizationScope);
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
||||
|
||||
public class GroupOperationRequirement : OperationAuthorizationRequirement
|
||||
{
|
||||
public GroupOperationRequirement(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
public static class GroupOperations
|
||||
{
|
||||
public static readonly GroupOperationRequirement ReadAll = new(nameof(ReadAll));
|
||||
public static readonly GroupOperationRequirement ReadAllDetails = new(nameof(ReadAllDetails));
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
#nullable enable
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -1,5 +1,5 @@
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
|
||||
using Bit.Core.Context;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||
@ -7,14 +7,10 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authoriza
|
||||
public class OrganizationUserUserMiniDetailsAuthorizationHandler :
|
||||
AuthorizationHandler<OrganizationUserUserMiniDetailsOperationRequirement, OrganizationScope>
|
||||
{
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
|
||||
public OrganizationUserUserMiniDetailsAuthorizationHandler(
|
||||
IApplicationCacheService applicationCacheService,
|
||||
ICurrentContext currentContext)
|
||||
public OrganizationUserUserMiniDetailsAuthorizationHandler(ICurrentContext currentContext)
|
||||
{
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_currentContext = currentContext;
|
||||
}
|
||||
|
||||
|
@ -4,5 +4,5 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interface
|
||||
|
||||
public interface IUpdateOrganizationUserGroupsCommand
|
||||
{
|
||||
Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds, Guid? loggedInUserId);
|
||||
Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds);
|
||||
}
|
||||
|
@ -9,25 +9,18 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
public class UpdateOrganizationUserGroupsCommand : IUpdateOrganizationUserGroupsCommand
|
||||
{
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
|
||||
public UpdateOrganizationUserGroupsCommand(
|
||||
IEventService eventService,
|
||||
IOrganizationService organizationService,
|
||||
IOrganizationUserRepository organizationUserRepository)
|
||||
{
|
||||
_eventService = eventService;
|
||||
_organizationService = organizationService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
}
|
||||
|
||||
public async Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds, Guid? loggedInUserId)
|
||||
public async Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds)
|
||||
{
|
||||
if (loggedInUserId.HasValue)
|
||||
{
|
||||
await _organizationService.ValidateOrganizationUserUpdatePermissions(organizationUser.OrganizationId, organizationUser.Type, null, organizationUser.GetPermissions());
|
||||
}
|
||||
await _organizationUserRepository.UpdateGroupsAsync(organizationUser.Id, groupIds);
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UpdatedGroups);
|
||||
}
|
||||
|
@ -18,5 +18,6 @@ public static class PolicyServiceCollectionExtensions
|
||||
services.AddScoped<IPolicyValidator, RequireSsoPolicyValidator>();
|
||||
services.AddScoped<IPolicyValidator, ResetPasswordPolicyValidator>();
|
||||
services.AddScoped<IPolicyValidator, MaximumVaultTimeoutPolicyValidator>();
|
||||
services.AddScoped<IPolicyValidator, FreeFamiliesForEnterprisePolicyValidator>();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,46 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
public class FreeFamiliesForEnterprisePolicyValidator(
|
||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
||||
IMailService mailService,
|
||||
IOrganizationRepository organizationRepository)
|
||||
: IPolicyValidator
|
||||
{
|
||||
public PolicyType Type => PolicyType.FreeFamiliesSponsorshipPolicy;
|
||||
public IEnumerable<PolicyType> RequiredPolicies => [];
|
||||
|
||||
public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||
{
|
||||
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
||||
{
|
||||
await NotifiesUserWithApplicablePoliciesAsync(policyUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task NotifiesUserWithApplicablePoliciesAsync(PolicyUpdate policy)
|
||||
{
|
||||
var organizationSponsorships = (await organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(policy.OrganizationId))
|
||||
.Where(p => p.SponsoredOrganizationId is not null)
|
||||
.ToList();
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(policy.OrganizationId);
|
||||
var organizationName = organization?.Name;
|
||||
|
||||
foreach (var org in organizationSponsorships)
|
||||
{
|
||||
var offerAcceptanceDate = org.ValidUntil!.Value.AddDays(-7).ToString("MM/dd/yyyy");
|
||||
await mailService.SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(org.FriendlyName, offerAcceptanceDate,
|
||||
org.SponsoredOrganizationId.ToString(), organizationName);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult("");
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// A typed wrapper for an organization Guid. This is used for authorization checks
|
@ -1,7 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
|
@ -15,6 +15,7 @@ using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
@ -444,13 +445,6 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup)
|
||||
{
|
||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
|
||||
if (!consolidatedBillingEnabled)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(SignupClientAsync)} is only for use within Consolidated Billing");
|
||||
}
|
||||
|
||||
var plan = StaticStore.GetPlan(signup.Plan);
|
||||
|
||||
ValidatePlan(plan, signup.AdditionalSeats, "Password Manager");
|
||||
@ -1444,10 +1438,7 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
if (provider is { Enabled: true })
|
||||
{
|
||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
|
||||
if (consolidatedBillingEnabled && provider.Type == ProviderType.Msp &&
|
||||
provider.Status == ProviderStatusType.Billable)
|
||||
if (provider.IsBillable())
|
||||
{
|
||||
return (false, "Seat limit has been reached. Please contact your provider to add more seats.");
|
||||
}
|
||||
|
@ -1,86 +0,0 @@
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Utilities.Duo;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
|
||||
public class DuoWebTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public DuoWebTokenProvider(
|
||||
IServiceProvider serviceProvider,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
||||
{
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
if (!(await userService.CanAccessPremium(user)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
|
||||
if (!HasProperMetaData(provider))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Duo, user);
|
||||
}
|
||||
|
||||
public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
||||
{
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
if (!(await userService.CanAccessPremium(user)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
|
||||
if (!HasProperMetaData(provider))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var signatureRequest = DuoWeb.SignRequest((string)provider.MetaData["IKey"],
|
||||
(string)provider.MetaData["SKey"], _globalSettings.Duo.AKey, user.Email);
|
||||
return signatureRequest;
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user)
|
||||
{
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
if (!(await userService.CanAccessPremium(user)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
|
||||
if (!HasProperMetaData(provider))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var response = DuoWeb.VerifyResponse((string)provider.MetaData["IKey"], (string)provider.MetaData["SKey"],
|
||||
_globalSettings.Duo.AKey, token);
|
||||
|
||||
return response == user.Email;
|
||||
}
|
||||
|
||||
private bool HasProperMetaData(TwoFactorProvider provider)
|
||||
{
|
||||
return provider?.MetaData != null && provider.MetaData.ContainsKey("IKey") &&
|
||||
provider.MetaData.ContainsKey("SKey") && provider.MetaData.ContainsKey("Host");
|
||||
}
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Utilities.Duo;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
|
||||
public interface IOrganizationDuoWebTokenProvider : IOrganizationTwoFactorTokenProvider { }
|
||||
|
||||
public class OrganizationDuoWebTokenProvider : IOrganizationDuoWebTokenProvider
|
||||
{
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public OrganizationDuoWebTokenProvider(GlobalSettings globalSettings)
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public Task<bool> CanGenerateTwoFactorTokenAsync(Organization organization)
|
||||
{
|
||||
if (organization == null || !organization.Enabled || !organization.Use2fa)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
|
||||
var canGenerate = organization.TwoFactorProviderIsEnabled(TwoFactorProviderType.OrganizationDuo)
|
||||
&& HasProperMetaData(provider);
|
||||
return Task.FromResult(canGenerate);
|
||||
}
|
||||
|
||||
public Task<string> GenerateAsync(Organization organization, User user)
|
||||
{
|
||||
if (organization == null || !organization.Enabled || !organization.Use2fa)
|
||||
{
|
||||
return Task.FromResult<string>(null);
|
||||
}
|
||||
|
||||
var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
|
||||
if (!HasProperMetaData(provider))
|
||||
{
|
||||
return Task.FromResult<string>(null);
|
||||
}
|
||||
|
||||
var signatureRequest = DuoWeb.SignRequest(provider.MetaData["IKey"].ToString(),
|
||||
provider.MetaData["SKey"].ToString(), _globalSettings.Duo.AKey, user.Email);
|
||||
return Task.FromResult(signatureRequest);
|
||||
}
|
||||
|
||||
public Task<bool> ValidateAsync(string token, Organization organization, User user)
|
||||
{
|
||||
if (organization == null || !organization.Enabled || !organization.Use2fa)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
|
||||
if (!HasProperMetaData(provider))
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
var response = DuoWeb.VerifyResponse(provider.MetaData["IKey"].ToString(),
|
||||
provider.MetaData["SKey"].ToString(), _globalSettings.Duo.AKey, token);
|
||||
|
||||
return Task.FromResult(response == user.Email);
|
||||
}
|
||||
|
||||
private bool HasProperMetaData(TwoFactorProvider provider)
|
||||
{
|
||||
return provider?.MetaData != null && provider.MetaData.ContainsKey("IKey") &&
|
||||
provider.MetaData.ContainsKey("SKey") && provider.MetaData.ContainsKey("Host");
|
||||
}
|
||||
}
|
@ -1,172 +0,0 @@
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Duo = DuoUniversal;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
|
||||
/*
|
||||
PM-5156 addresses tech debt
|
||||
Interface to allow for DI, will end up being removed as part of the removal of the old Duo SDK v2 flows.
|
||||
This service is to support SDK v4 flows for Duo. At some time in the future we will need
|
||||
to combine this service with the DuoWebTokenProvider and OrganizationDuoWebTokenProvider to support SDK v4.
|
||||
*/
|
||||
public interface ITemporaryDuoWebV4SDKService
|
||||
{
|
||||
Task<string> GenerateAsync(TwoFactorProvider provider, User user);
|
||||
Task<bool> ValidateAsync(string token, TwoFactorProvider provider, User user);
|
||||
}
|
||||
|
||||
public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory;
|
||||
private readonly ILogger<TemporaryDuoWebV4SDKService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for the DuoUniversalPromptService. Used to supplement v2 implementation of Duo with v4 SDK
|
||||
/// </summary>
|
||||
/// <param name="currentContext">used to fetch initiating Client</param>
|
||||
/// <param name="globalSettings">used to fetch vault URL for Redirect URL</param>
|
||||
public TemporaryDuoWebV4SDKService(
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,
|
||||
ILogger<TemporaryDuoWebV4SDKService> logger)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_globalSettings = globalSettings;
|
||||
_tokenDataFactory = tokenDataFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provider agnostic (either Duo or OrganizationDuo) method to generate a Duo Auth URL
|
||||
/// </summary>
|
||||
/// <param name="provider">Either Duo or OrganizationDuo</param>
|
||||
/// <param name="user">self</param>
|
||||
/// <returns>AuthUrl for DUO SDK v4</returns>
|
||||
public async Task<string> GenerateAsync(TwoFactorProvider provider, User user)
|
||||
{
|
||||
if (!HasProperMetaData(provider))
|
||||
{
|
||||
if (!HasProperMetaData_SDKV2(provider))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var duoClient = await BuildDuoClientAsync(provider);
|
||||
if (duoClient == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var state = _tokenDataFactory.Protect(new DuoUserStateTokenable(user));
|
||||
var authUrl = duoClient.GenerateAuthUri(user.Email, state);
|
||||
|
||||
return authUrl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates Duo SDK v4 response
|
||||
/// </summary>
|
||||
/// <param name="token">response form Duo</param>
|
||||
/// <param name="provider">TwoFactorProviderType Duo or OrganizationDuo</param>
|
||||
/// <param name="user">self</param>
|
||||
/// <returns>true or false depending on result of verification</returns>
|
||||
public async Task<bool> ValidateAsync(string token, TwoFactorProvider provider, User user)
|
||||
{
|
||||
if (!HasProperMetaData(provider))
|
||||
{
|
||||
if (!HasProperMetaData_SDKV2(provider))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var duoClient = await BuildDuoClientAsync(provider);
|
||||
if (duoClient == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var parts = token.Split("|");
|
||||
var authCode = parts[0];
|
||||
var state = parts[1];
|
||||
|
||||
_tokenDataFactory.TryUnprotect(state, out var tokenable);
|
||||
if (!tokenable.Valid || !tokenable.TokenIsValid(user))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// duoClient compares the email from the received IdToken with user.Email to verify a bad actor hasn't used
|
||||
// their authCode with a victims credentials
|
||||
var res = await duoClient.ExchangeAuthorizationCodeFor2faResult(authCode, user.Email);
|
||||
// If the result of the exchange doesn't throw an exception and it's not null, then it's valid
|
||||
return res.AuthResult.Result == "allow";
|
||||
}
|
||||
|
||||
private bool HasProperMetaData(TwoFactorProvider provider)
|
||||
{
|
||||
return provider?.MetaData != null && provider.MetaData.ContainsKey("ClientId") &&
|
||||
provider.MetaData.ContainsKey("ClientSecret") && provider.MetaData.ContainsKey("Host");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the metadata for SDK V2 is present.
|
||||
/// Transitional method to support Duo during v4 database rename
|
||||
/// </summary>
|
||||
/// <param name="provider">The TwoFactorProvider object to check.</param>
|
||||
/// <returns>True if the provider has the proper metadata; otherwise, false.</returns>
|
||||
private bool HasProperMetaData_SDKV2(TwoFactorProvider provider)
|
||||
{
|
||||
if (provider?.MetaData != null &&
|
||||
provider.MetaData.TryGetValue("IKey", out var iKey) &&
|
||||
provider.MetaData.TryGetValue("SKey", out var sKey) &&
|
||||
provider.MetaData.ContainsKey("Host"))
|
||||
{
|
||||
provider.MetaData.Add("ClientId", iKey);
|
||||
provider.MetaData.Add("ClientSecret", sKey);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Duo.Client object for use with Duo SDK v4. This combines the health check and the client generation
|
||||
/// </summary>
|
||||
/// <param name="provider">TwoFactorProvider Duo or OrganizationDuo</param>
|
||||
/// <returns>Duo.Client object or null</returns>
|
||||
private async Task<Duo.Client> BuildDuoClientAsync(TwoFactorProvider provider)
|
||||
{
|
||||
// Fetch Client name from header value since duo auth can be initiated from multiple clients and we want
|
||||
// to redirect back to the initiating client
|
||||
_currentContext.HttpContext.Request.Headers.TryGetValue("Bitwarden-Client-Name", out var bitwardenClientName);
|
||||
var redirectUri = string.Format("{0}/duo-redirect-connector.html?client={1}",
|
||||
_globalSettings.BaseServiceUri.Vault, bitwardenClientName.FirstOrDefault() ?? "web");
|
||||
|
||||
var client = new Duo.ClientBuilder(
|
||||
(string)provider.MetaData["ClientId"],
|
||||
(string)provider.MetaData["ClientSecret"],
|
||||
(string)provider.MetaData["Host"],
|
||||
redirectUri).Build();
|
||||
|
||||
if (!await client.DoHealthCheck(true))
|
||||
{
|
||||
_logger.LogError("Unable to connect to Duo. Health check failed.");
|
||||
return null;
|
||||
}
|
||||
return client;
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OtpNet;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
{
|
@ -0,0 +1,102 @@
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tokens;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Duo = DuoUniversal;
|
||||
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public class DuoUniversalTokenProvider(
|
||||
IServiceProvider serviceProvider,
|
||||
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,
|
||||
IDuoUniversalTokenService duoUniversalTokenService) : IUserTwoFactorTokenProvider<User>
|
||||
{
|
||||
/// <summary>
|
||||
/// We need the IServiceProvider to resolve the IUserService. There is a complex dependency dance
|
||||
/// occurring between IUserService, which extends the UserManager<User>, and the usage of the
|
||||
/// UserManager<User> within this class. Trying to resolve the IUserService using the DI pipeline
|
||||
/// will not allow the server to start and it will hang and give no helpful indication as to the problem.
|
||||
/// </summary>
|
||||
private readonly IServiceProvider _serviceProvider = serviceProvider;
|
||||
private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory = tokenDataFactory;
|
||||
private readonly IDuoUniversalTokenService _duoUniversalTokenService = duoUniversalTokenService;
|
||||
|
||||
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
||||
{
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
var provider = await GetDuoTwoFactorProvider(user, userService);
|
||||
if (provider == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Duo, user);
|
||||
}
|
||||
|
||||
public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
||||
{
|
||||
var duoClient = await GetDuoClientAsync(user);
|
||||
if (duoClient == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return _duoUniversalTokenService.GenerateAuthUrl(duoClient, _tokenDataFactory, user);
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user)
|
||||
{
|
||||
var duoClient = await GetDuoClientAsync(user);
|
||||
if (duoClient == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return await _duoUniversalTokenService.RequestDuoValidationAsync(duoClient, _tokenDataFactory, user, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the Duo Two Factor Provider for the user if they have access to Duo
|
||||
/// </summary>
|
||||
/// <param name="user">Active User</param>
|
||||
/// <returns>null or Duo TwoFactorProvider</returns>
|
||||
private async Task<TwoFactorProvider> GetDuoTwoFactorProvider(User user, IUserService userService)
|
||||
{
|
||||
if (!await userService.CanAccessPremium(user))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
|
||||
if (!_duoUniversalTokenService.HasProperDuoMetadata(provider))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses the User to fetch a valid TwoFactorProvider and use it to create a Duo.Client
|
||||
/// </summary>
|
||||
/// <param name="user">active user</param>
|
||||
/// <returns>null or Duo TwoFactorProvider</returns>
|
||||
private async Task<Duo.Client> GetDuoClientAsync(User user)
|
||||
{
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
var provider = await GetDuoTwoFactorProvider(user, userService);
|
||||
if (provider == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var duoClient = await _duoUniversalTokenService.BuildDuoTwoFactorClientAsync(provider);
|
||||
if (duoClient == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return duoClient;
|
||||
}
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Duo = DuoUniversal;
|
||||
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
/// <summary>
|
||||
/// OrganizationDuo and Duo TwoFactorProviderTypes both use the same flows so both of those Token Providers will
|
||||
/// have this class injected to utilize these methods
|
||||
/// </summary>
|
||||
public interface IDuoUniversalTokenService
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates the Duo Auth URL for the user to be redirected to Duo for 2FA. This
|
||||
/// Auth URL also lets the Duo Service know where to redirect the user back to after
|
||||
/// the 2FA process is complete.
|
||||
/// </summary>
|
||||
/// <param name="duoClient">A not null valid Duo.Client</param>
|
||||
/// <param name="tokenDataFactory">This service creates the state token for added security</param>
|
||||
/// <param name="user">currently active user</param>
|
||||
/// <returns>a URL in string format</returns>
|
||||
string GenerateAuthUrl(
|
||||
Duo.Client duoClient,
|
||||
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,
|
||||
User user);
|
||||
|
||||
/// <summary>
|
||||
/// Makes the request to Duo to validate the authCode and state token
|
||||
/// </summary>
|
||||
/// <param name="duoClient">A not null valid Duo.Client</param>
|
||||
/// <param name="tokenDataFactory">Factory for decrypting the state</param>
|
||||
/// <param name="user">self</param>
|
||||
/// <param name="token">token received from the client</param>
|
||||
/// <returns>boolean based on result from Duo</returns>
|
||||
Task<bool> RequestDuoValidationAsync(
|
||||
Duo.Client duoClient,
|
||||
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,
|
||||
User user,
|
||||
string token);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Duo.Client object for use with Duo SDK v4. This method is to validate a Duo configuration
|
||||
/// when adding or updating the configuration. This method makes a web request to Duo to verify the configuration.
|
||||
/// Throws exception if configuration is invalid.
|
||||
/// </summary>
|
||||
/// <param name="clientSecret">Duo client Secret</param>
|
||||
/// <param name="clientId">Duo client Id</param>
|
||||
/// <param name="host">Duo host</param>
|
||||
/// <returns>Boolean</returns>
|
||||
Task<bool> ValidateDuoConfiguration(string clientSecret, string clientId, string host);
|
||||
|
||||
/// <summary>
|
||||
/// Checks provider for the correct Duo metadata: ClientId, ClientSecret, and Host. Does no validation on the data.
|
||||
/// it is assumed to be correct. The only way to have the data written to the Database is after verification
|
||||
/// occurs.
|
||||
/// </summary>
|
||||
/// <param name="provider">Host being checked for proper data</param>
|
||||
/// <returns>true if all three are present; false if one is missing or the host is incorrect</returns>
|
||||
bool HasProperDuoMetadata(TwoFactorProvider provider);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Duo.Client object for use with Duo SDK v4. This combines the health check and the client generation.
|
||||
/// This method is made public so that it is easier to test. If the method was private then there would not be an
|
||||
/// easy way to mock the response. Since this makes a web request it is difficult to mock.
|
||||
/// </summary>
|
||||
/// <param name="provider">TwoFactorProvider Duo or OrganizationDuo</param>
|
||||
/// <returns>Duo.Client object or null</returns>
|
||||
Task<Duo.Client> BuildDuoTwoFactorClientAsync(TwoFactorProvider provider);
|
||||
}
|
||||
|
||||
public class DuoUniversalTokenService(
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings) : IDuoUniversalTokenService
|
||||
{
|
||||
private readonly ICurrentContext _currentContext = currentContext;
|
||||
private readonly GlobalSettings _globalSettings = globalSettings;
|
||||
|
||||
public string GenerateAuthUrl(
|
||||
Duo.Client duoClient,
|
||||
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,
|
||||
User user)
|
||||
{
|
||||
var state = tokenDataFactory.Protect(new DuoUserStateTokenable(user));
|
||||
var authUrl = duoClient.GenerateAuthUri(user.Email, state);
|
||||
|
||||
return authUrl;
|
||||
}
|
||||
|
||||
public async Task<bool> RequestDuoValidationAsync(
|
||||
Duo.Client duoClient,
|
||||
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,
|
||||
User user,
|
||||
string token)
|
||||
{
|
||||
var parts = token.Split("|");
|
||||
var authCode = parts[0];
|
||||
var state = parts[1];
|
||||
tokenDataFactory.TryUnprotect(state, out var tokenable);
|
||||
if (!tokenable.Valid || !tokenable.TokenIsValid(user))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// duoClient compares the email from the received IdToken with user.Email to verify a bad actor hasn't used
|
||||
// their authCode with a victims credentials
|
||||
var res = await duoClient.ExchangeAuthorizationCodeFor2faResult(authCode, user.Email);
|
||||
// If the result of the exchange doesn't throw an exception and it's not null, then it's valid
|
||||
return res.AuthResult.Result == "allow";
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateDuoConfiguration(string clientSecret, string clientId, string host)
|
||||
{
|
||||
// Do some simple checks to ensure data integrity
|
||||
if (!ValidDuoHost(host) ||
|
||||
string.IsNullOrWhiteSpace(clientSecret) ||
|
||||
string.IsNullOrWhiteSpace(clientId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// The AuthURI is not important for this health check so we pass in a non-empty string
|
||||
var client = new Duo.ClientBuilder(clientId, clientSecret, host, "non-empty").Build();
|
||||
|
||||
// This could throw an exception, the false flag will allow the exception to bubble up
|
||||
return await client.DoHealthCheck(false);
|
||||
}
|
||||
|
||||
public bool HasProperDuoMetadata(TwoFactorProvider provider)
|
||||
{
|
||||
return provider?.MetaData != null &&
|
||||
provider.MetaData.ContainsKey("ClientId") &&
|
||||
provider.MetaData.ContainsKey("ClientSecret") &&
|
||||
provider.MetaData.ContainsKey("Host") &&
|
||||
ValidDuoHost((string)provider.MetaData["Host"]);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Checks the host string to make sure it meets Duo's Guidelines before attempting to create a Duo.Client.
|
||||
/// </summary>
|
||||
/// <param name="host">string representing the Duo Host</param>
|
||||
/// <returns>true if the host is valid false otherwise</returns>
|
||||
public static bool ValidDuoHost(string host)
|
||||
{
|
||||
if (Uri.TryCreate($"https://{host}", UriKind.Absolute, out var uri))
|
||||
{
|
||||
return (string.IsNullOrWhiteSpace(uri.PathAndQuery) || uri.PathAndQuery == "/") &&
|
||||
uri.Host.StartsWith("api-") &&
|
||||
(uri.Host.EndsWith(".duosecurity.com") || uri.Host.EndsWith(".duofederal.com"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<Duo.Client> BuildDuoTwoFactorClientAsync(TwoFactorProvider provider)
|
||||
{
|
||||
// Fetch Client name from header value since duo auth can be initiated from multiple clients and we want
|
||||
// to redirect back to the initiating client
|
||||
_currentContext.HttpContext.Request.Headers.TryGetValue("Bitwarden-Client-Name", out var bitwardenClientName);
|
||||
var redirectUri = string.Format("{0}/duo-redirect-connector.html?client={1}",
|
||||
_globalSettings.BaseServiceUri.Vault, bitwardenClientName.FirstOrDefault() ?? "web");
|
||||
|
||||
var client = new Duo.ClientBuilder(
|
||||
(string)provider.MetaData["ClientId"],
|
||||
(string)provider.MetaData["ClientSecret"],
|
||||
(string)provider.MetaData["Host"],
|
||||
redirectUri).Build();
|
||||
|
||||
if (!await client.DoHealthCheck(false))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return client;
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public class EmailTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
{
|
@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public class EmailTwoFactorTokenProvider : EmailTokenProvider
|
||||
{
|
@ -1,7 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public interface IOrganizationTwoFactorTokenProvider
|
||||
{
|
@ -0,0 +1,81 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Tokens;
|
||||
using Duo = DuoUniversal;
|
||||
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public interface IOrganizationDuoUniversalTokenProvider : IOrganizationTwoFactorTokenProvider { }
|
||||
|
||||
public class OrganizationDuoUniversalTokenProvider(
|
||||
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,
|
||||
IDuoUniversalTokenService duoUniversalTokenService) : IOrganizationDuoUniversalTokenProvider
|
||||
{
|
||||
private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory = tokenDataFactory;
|
||||
private readonly IDuoUniversalTokenService _duoUniversalTokenService = duoUniversalTokenService;
|
||||
|
||||
public Task<bool> CanGenerateTwoFactorTokenAsync(Organization organization)
|
||||
{
|
||||
var provider = GetDuoTwoFactorProvider(organization);
|
||||
if (provider != null && provider.Enabled)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
public async Task<string> GenerateAsync(Organization organization, User user)
|
||||
{
|
||||
var duoClient = await GetDuoClientAsync(organization);
|
||||
if (duoClient == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return _duoUniversalTokenService.GenerateAuthUrl(duoClient, _tokenDataFactory, user);
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateAsync(string token, Organization organization, User user)
|
||||
{
|
||||
var duoClient = await GetDuoClientAsync(organization);
|
||||
if (duoClient == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return await _duoUniversalTokenService.RequestDuoValidationAsync(duoClient, _tokenDataFactory, user, token);
|
||||
}
|
||||
|
||||
private TwoFactorProvider GetDuoTwoFactorProvider(Organization organization)
|
||||
{
|
||||
if (organization == null || !organization.Enabled || !organization.Use2fa)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
|
||||
if (!_duoUniversalTokenService.HasProperDuoMetadata(provider))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
private async Task<Duo.Client> GetDuoClientAsync(Organization organization)
|
||||
{
|
||||
var provider = GetDuoTwoFactorProvider(organization);
|
||||
if (provider == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var duoClient = await _duoUniversalTokenService.BuildDuoTwoFactorClientAsync(provider);
|
||||
if (duoClient == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return duoClient;
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public class TwoFactorRememberTokenProvider : DataProtectorTokenProvider<User>
|
||||
{
|
@ -10,7 +10,7 @@ using Fido2NetLib.Objects;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
{
|
@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using YubicoDotNetClient;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public class YubicoOtpTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
{
|
||||
@ -24,7 +24,7 @@ public class YubicoOtpTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
||||
{
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
if (!(await userService.CanAccessPremium(user)))
|
||||
if (!await userService.CanAccessPremium(user))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -46,7 +46,7 @@ public class YubicoOtpTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
public async Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user)
|
||||
{
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
if (!(await userService.CanAccessPremium(user)))
|
||||
if (!await userService.CanAccessPremium(user))
|
||||
{
|
||||
return false;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
|
||||
#nullable enable
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
#nullable enable
|
||||
|
@ -5,12 +5,12 @@ using Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey.Implementations;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.KeyManagement.UserKey.Implementations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user