mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
Merge branch 'main' into ac/pm-10338/leave-endpoint-to-log-organizationuser_left
This commit is contained in:
commit
9fcb06d647
35
.github/workflows/repository-management.yml
vendored
35
.github/workflows/repository-management.yml
vendored
@ -28,7 +28,6 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
outputs:
|
outputs:
|
||||||
branch: ${{ steps.set-branch.outputs.branch }}
|
branch: ${{ steps.set-branch.outputs.branch }}
|
||||||
token: ${{ steps.app-token.outputs.token }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set branch
|
- name: Set branch
|
||||||
id: set-branch
|
id: set-branch
|
||||||
@ -45,13 +44,6 @@ jobs:
|
|||||||
|
|
||||||
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
|
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:
|
cut_branch:
|
||||||
name: Cut branch
|
name: Cut branch
|
||||||
@ -59,11 +51,18 @@ jobs:
|
|||||||
needs: setup
|
needs: setup
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
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
|
- name: Check out target ref
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.target_ref }}
|
ref: ${{ inputs.target_ref }}
|
||||||
token: ${{ needs.setup.outputs.token }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
|
||||||
- name: Check if ${{ needs.setup.outputs.branch }} branch exists
|
- name: Check if ${{ needs.setup.outputs.branch }} branch exists
|
||||||
env:
|
env:
|
||||||
@ -98,11 +97,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: ${{ inputs.version_number_override }}
|
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
|
- name: Check out branch
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
token: ${{ needs.setup.outputs.token }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
|
||||||
- name: Configure Git
|
- name: Configure Git
|
||||||
run: |
|
run: |
|
||||||
@ -190,11 +196,18 @@ jobs:
|
|||||||
- bump_version
|
- bump_version
|
||||||
- setup
|
- setup
|
||||||
steps:
|
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
|
- name: Check out main branch
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
token: ${{ needs.setup.outputs.token }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
|
||||||
- name: Configure Git
|
- name: Configure Git
|
||||||
run: |
|
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.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
@ -10,7 +9,6 @@ using Bit.Core.Billing.Repositories;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.AdminConsole.Providers;
|
namespace Bit.Commercial.Core.AdminConsole.Providers;
|
||||||
|
|
||||||
@ -21,35 +19,28 @@ public class CreateProviderCommand : ICreateProviderCommand
|
|||||||
private readonly IProviderService _providerService;
|
private readonly IProviderService _providerService;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly IProviderPlanRepository _providerPlanRepository;
|
private readonly IProviderPlanRepository _providerPlanRepository;
|
||||||
private readonly IFeatureService _featureService;
|
|
||||||
|
|
||||||
public CreateProviderCommand(
|
public CreateProviderCommand(
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
IProviderUserRepository providerUserRepository,
|
IProviderUserRepository providerUserRepository,
|
||||||
IProviderService providerService,
|
IProviderService providerService,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IProviderPlanRepository providerPlanRepository,
|
IProviderPlanRepository providerPlanRepository)
|
||||||
IFeatureService featureService)
|
|
||||||
{
|
{
|
||||||
_providerRepository = providerRepository;
|
_providerRepository = providerRepository;
|
||||||
_providerUserRepository = providerUserRepository;
|
_providerUserRepository = providerUserRepository;
|
||||||
_providerService = providerService;
|
_providerService = providerService;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_providerPlanRepository = providerPlanRepository;
|
_providerPlanRepository = providerPlanRepository;
|
||||||
_featureService = featureService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats)
|
public async Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats)
|
||||||
{
|
{
|
||||||
var providerId = await CreateProviderAsync(provider, ownerEmail);
|
var providerId = await CreateProviderAsync(provider, ownerEmail);
|
||||||
|
|
||||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
await Task.WhenAll(
|
||||||
|
CreateProviderPlanAsync(providerId, PlanType.TeamsMonthly, teamsMinimumSeats),
|
||||||
if (isConsolidatedBillingEnabled)
|
CreateProviderPlanAsync(providerId, PlanType.EnterpriseMonthly, enterpriseMinimumSeats));
|
||||||
{
|
|
||||||
await CreateProviderPlanAsync(providerId, PlanType.TeamsMonthly, teamsMinimumSeats);
|
|
||||||
await CreateProviderPlanAsync(providerId, PlanType.EnterpriseMonthly, enterpriseMinimumSeats);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateResellerAsync(Provider provider)
|
public async Task CreateResellerAsync(Provider provider)
|
||||||
@ -61,12 +52,7 @@ public class CreateProviderCommand : ICreateProviderCommand
|
|||||||
{
|
{
|
||||||
var providerId = await CreateProviderAsync(provider, ownerEmail);
|
var providerId = await CreateProviderAsync(provider, ownerEmail);
|
||||||
|
|
||||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
await CreateProviderPlanAsync(providerId, plan, minimumSeats);
|
||||||
|
|
||||||
if (isConsolidatedBillingEnabled)
|
|
||||||
{
|
|
||||||
await CreateProviderPlanAsync(providerId, plan, minimumSeats);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Guid> CreateProviderAsync(Provider provider, string ownerEmail)
|
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.");
|
throw new BadRequestException("Invalid owner. Owner must be an existing Bitwarden user.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
provider.Gateway = GatewayType.Stripe;
|
||||||
|
|
||||||
if (isConsolidatedBillingEnabled)
|
|
||||||
{
|
|
||||||
provider.Gateway = GatewayType.Stripe;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Pending);
|
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.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
@ -102,11 +100,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
IEnumerable<string> organizationOwnerEmails)
|
IEnumerable<string> organizationOwnerEmails)
|
||||||
{
|
{
|
||||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
if (provider.IsBillable() &&
|
||||||
|
organization.IsValidClient() &&
|
||||||
if (isConsolidatedBillingEnabled &&
|
|
||||||
provider.Status == ProviderStatusType.Billable &&
|
|
||||||
organization.Status == OrganizationStatusType.Managed &&
|
|
||||||
!string.IsNullOrEmpty(organization.GatewayCustomerId))
|
!string.IsNullOrEmpty(organization.GatewayCustomerId))
|
||||||
{
|
{
|
||||||
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
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.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
@ -101,24 +100,16 @@ public class ProviderService : IProviderService
|
|||||||
throw new BadRequestException("Invalid owner.");
|
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;
|
throw new BadRequestException("Both address and postal code are required to set up your provider.");
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
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;
|
providerUser.Key = key;
|
||||||
await _providerUserRepository.ReplaceAsync(providerUser);
|
await _providerUserRepository.ReplaceAsync(providerUser);
|
||||||
@ -545,13 +536,9 @@ public class ProviderService : IProviderService
|
|||||||
{
|
{
|
||||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
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) = await _organizationService.SignupClientAsync(organizationSignup);
|
||||||
|
|
||||||
var (organization, _, defaultCollection) = consolidatedBillingEnabled
|
|
||||||
? await _organizationService.SignupClientAsync(organizationSignup)
|
|
||||||
: await _organizationService.SignUpAsync(organizationSignup);
|
|
||||||
|
|
||||||
var providerOrganization = new ProviderOrganization
|
var providerOrganization = new ProviderOrganization
|
||||||
{
|
{
|
||||||
@ -687,27 +674,24 @@ public class ProviderService : IProviderService
|
|||||||
return confirmedOwnersIds.Except(providerUserIds).Any();
|
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))
|
||||||
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.");
|
||||||
{
|
}
|
||||||
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:
|
||||||
break;
|
if (requestedType is not (PlanType.EnterpriseMonthly or PlanType.EnterpriseAnnually))
|
||||||
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.");
|
||||||
{
|
}
|
||||||
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:
|
||||||
break;
|
throw new BadRequestException($"Unsupported provider type {providerType}.");
|
||||||
default:
|
|
||||||
throw new BadRequestException($"Unsupported provider type {providerType}.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ProviderDisallowedOrganizationTypes.Contains(requestedType))
|
if (ProviderDisallowedOrganizationTypes.Contains(requestedType))
|
||||||
|
@ -2,17 +2,14 @@
|
|||||||
using Bit.Commercial.Core.Billing.Models;
|
using Bit.Commercial.Core.Billing.Models;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing;
|
using Bit.Core.Billing;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Entities;
|
using Bit.Core.Billing.Entities;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
using Bit.Core.Context;
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
@ -27,7 +24,6 @@ using Stripe;
|
|||||||
namespace Bit.Commercial.Core.Billing;
|
namespace Bit.Commercial.Core.Billing;
|
||||||
|
|
||||||
public class ProviderBillingService(
|
public class ProviderBillingService(
|
||||||
ICurrentContext currentContext,
|
|
||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
ILogger<ProviderBillingService> logger,
|
ILogger<ProviderBillingService> logger,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@ -35,38 +31,76 @@ public class ProviderBillingService(
|
|||||||
IProviderInvoiceItemRepository providerInvoiceItemRepository,
|
IProviderInvoiceItemRepository providerInvoiceItemRepository,
|
||||||
IProviderOrganizationRepository providerOrganizationRepository,
|
IProviderOrganizationRepository providerOrganizationRepository,
|
||||||
IProviderPlanRepository providerPlanRepository,
|
IProviderPlanRepository providerPlanRepository,
|
||||||
IProviderRepository providerRepository,
|
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
ISubscriberService subscriberService) : IProviderBillingService
|
ISubscriberService subscriberService) : IProviderBillingService
|
||||||
{
|
{
|
||||||
public async Task AssignSeatsToClientOrganization(
|
public async Task ChangePlan(ChangeProviderPlanCommand command)
|
||||||
Provider provider,
|
|
||||||
Organization organization,
|
|
||||||
int seats)
|
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(organization);
|
var plan = await providerPlanRepository.GetByIdAsync(command.ProviderPlanId);
|
||||||
|
|
||||||
if (seats < 0)
|
if (plan == null)
|
||||||
{
|
{
|
||||||
throw new BillingException(
|
throw new BadRequestException("Provider plan not found.");
|
||||||
"You cannot assign negative seats to a client.",
|
|
||||||
"MSP cannot assign negative seats to a client organization");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
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(
|
public async Task CreateCustomerForClientOrganization(
|
||||||
@ -171,65 +205,16 @@ public class ProviderBillingService(
|
|||||||
return memoryStream.ToArray();
|
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(
|
public async Task ScaleSeats(
|
||||||
Provider provider,
|
Provider provider,
|
||||||
PlanType planType,
|
PlanType planType,
|
||||||
int seatAdjustment)
|
int seatAdjustment)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(provider);
|
var providerPlan = await GetProviderPlanAsync(provider, planType);
|
||||||
|
|
||||||
if (!provider.SupportsConsolidatedBilling())
|
var seatMinimum = providerPlan.SeatMinimum ?? 0;
|
||||||
{
|
|
||||||
logger.LogError("Provider ({ProviderID}) cannot scale their seats", provider.Id);
|
|
||||||
|
|
||||||
throw new BillingException();
|
var currentlyAssignedSeatTotal = await GetAssignedSeatTotalAsync(provider, planType);
|
||||||
}
|
|
||||||
|
|
||||||
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 newlyAssignedSeatTotal = currentlyAssignedSeatTotal + seatAdjustment;
|
var newlyAssignedSeatTotal = currentlyAssignedSeatTotal + seatAdjustment;
|
||||||
|
|
||||||
@ -256,13 +241,6 @@ public class ProviderBillingService(
|
|||||||
else if (currentlyAssignedSeatTotal <= seatMinimum &&
|
else if (currentlyAssignedSeatTotal <= seatMinimum &&
|
||||||
newlyAssignedSeatTotal > 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(
|
await update(
|
||||||
seatMinimum,
|
seatMinimum,
|
||||||
newlyAssignedSeatTotal);
|
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(
|
public async Task<Customer> SetupCustomer(
|
||||||
Provider provider,
|
Provider provider,
|
||||||
TaxInfo taxInfo)
|
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)
|
public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command)
|
||||||
{
|
{
|
||||||
if (command.Configuration.Any(x => x.SeatsMinimum < 0))
|
if (command.Configuration.Any(x => x.SeatsMinimum < 0))
|
||||||
@ -610,4 +539,32 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
await providerPlanRepository.ReplaceAsync(providerPlan);
|
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()
|
@RenderBody()
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container footer text-muted">
|
<div class="container footer text-body-secondary">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
© @DateTime.Now.Year, Bitwarden Inc.
|
© @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",
|
"version": "0.0.0",
|
||||||
"license": "-",
|
"license": "-",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "4.6.2",
|
"bootstrap": "5.3.3",
|
||||||
"font-awesome": "4.7.0",
|
"font-awesome": "4.7.0",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1"
|
||||||
"popper.js": "1.16.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"css-loader": "7.1.2",
|
"css-loader": "7.1.2",
|
||||||
@ -384,6 +383,17 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"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": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||||
@ -702,9 +712,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bootstrap": {
|
"node_modules/bootstrap": {
|
||||||
"version": "4.6.2",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
|
||||||
"integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==",
|
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@ -715,10 +725,8 @@
|
|||||||
"url": "https://opencollective.com/bootstrap"
|
"url": "https://opencollective.com/bootstrap"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"jquery": "1.9.1 - 3",
|
"@popperjs/core": "^2.11.8"
|
||||||
"popper.js": "^1.16.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/braces": {
|
"node_modules/braces": {
|
||||||
@ -1577,17 +1585,6 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.47",
|
"version": "8.4.47",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||||
|
@ -8,10 +8,9 @@
|
|||||||
"build": "webpack"
|
"build": "webpack"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "4.6.2",
|
"bootstrap": "5.3.3",
|
||||||
"font-awesome": "4.7.0",
|
"font-awesome": "4.7.0",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1"
|
||||||
"popper.js": "1.16.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"css-loader": "7.1.2",
|
"css-loader": "7.1.2",
|
||||||
|
@ -13,8 +13,6 @@ module.exports = {
|
|||||||
entry: {
|
entry: {
|
||||||
site: [
|
site: [
|
||||||
path.resolve(__dirname, paths.sassDir, "site.scss"),
|
path.resolve(__dirname, paths.sassDir, "site.scss"),
|
||||||
|
|
||||||
"popper.js",
|
|
||||||
"bootstrap",
|
"bootstrap",
|
||||||
"jquery",
|
"jquery",
|
||||||
"font-awesome/css/font-awesome.css",
|
"font-awesome/css/font-awesome.css",
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using Bit.Commercial.Core.AdminConsole.Providers;
|
using Bit.Commercial.Core.AdminConsole.Providers;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
@ -155,9 +154,6 @@ public class RemoveOrganizationFromProviderCommandTests
|
|||||||
"b@example.com"
|
"b@example.com"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
|
||||||
.Returns(false);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
|
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
|
||||||
.Returns(GetSubscription(organization.GatewaySubscriptionId));
|
.Returns(GetSubscription(organization.GatewaySubscriptionId));
|
||||||
|
|
||||||
@ -222,9 +218,6 @@ public class RemoveOrganizationFromProviderCommandTests
|
|||||||
"b@example.com"
|
"b@example.com"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
|
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using Bit.Commercial.Core.AdminConsole.Services;
|
using Bit.Commercial.Core.AdminConsole.Services;
|
||||||
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
|
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
@ -55,36 +54,8 @@ public class ProviderServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key,
|
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TaxInfo taxInfo,
|
||||||
[ProviderUser(ProviderUserStatusType.Confirmed, ProviderUserType.ProviderAdmin)] ProviderUser providerUser,
|
[ProviderUser] ProviderUser providerUser,
|
||||||
SutProvider<ProviderService> sutProvider)
|
|
||||||
{
|
|
||||||
providerUser.ProviderId = provider.Id;
|
|
||||||
providerUser.UserId = user.Id;
|
|
||||||
var userService = sutProvider.GetDependency<IUserService>();
|
|
||||||
userService.GetUserByIdAsync(user.Id).Returns(user);
|
|
||||||
|
|
||||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
|
||||||
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
|
|
||||||
|
|
||||||
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
|
|
||||||
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
|
||||||
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
|
|
||||||
.Returns(protector);
|
|
||||||
sutProvider.Create();
|
|
||||||
|
|
||||||
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
|
||||||
|
|
||||||
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,
|
|
||||||
SutProvider<ProviderService> sutProvider)
|
SutProvider<ProviderService> sutProvider)
|
||||||
{
|
{
|
||||||
providerUser.ProviderId = provider.Id;
|
providerUser.ProviderId = provider.Id;
|
||||||
@ -100,9 +71,6 @@ public class ProviderServiceTests
|
|||||||
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
|
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
|
||||||
.Returns(protector);
|
.Returns(protector);
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
|
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
|
||||||
|
|
||||||
var customer = new Customer { Id = "customer_id" };
|
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,
|
public async Task AddOrganization_OrganizationHasSecretsManager_Throws(Provider provider, Organization organization, string key,
|
||||||
SutProvider<ProviderService> sutProvider)
|
SutProvider<ProviderService> sutProvider)
|
||||||
{
|
{
|
||||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
organization.PlanType = PlanType.EnterpriseMonthly;
|
||||||
organization.UseSecretsManager = true;
|
organization.UseSecretsManager = true;
|
||||||
|
|
||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
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,
|
public async Task AddOrganization_Success(Provider provider, Organization organization, string key,
|
||||||
SutProvider<ProviderService> sutProvider)
|
SutProvider<ProviderService> sutProvider)
|
||||||
{
|
{
|
||||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
organization.PlanType = PlanType.EnterpriseMonthly;
|
||||||
|
|
||||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
@ -549,8 +517,8 @@ public class ProviderServiceTests
|
|||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
var expectedPlanType = PlanType.EnterpriseAnnually;
|
var expectedPlanType = PlanType.EnterpriseMonthly;
|
||||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
organization.PlanType = PlanType.EnterpriseMonthly;
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
|
||||||
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
||||||
@ -579,12 +547,12 @@ public class ProviderServiceTests
|
|||||||
BackdateProviderCreationDate(provider, newCreationDate);
|
BackdateProviderCreationDate(provider, newCreationDate);
|
||||||
provider.Type = ProviderType.Msp;
|
provider.Type = ProviderType.Msp;
|
||||||
|
|
||||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
organization.PlanType = PlanType.EnterpriseMonthly;
|
||||||
organization.Plan = "Enterprise (Annually)";
|
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);
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
@ -663,11 +631,11 @@ public class ProviderServiceTests
|
|||||||
public async Task CreateOrganizationAsync_Success(Provider provider, OrganizationSignup organizationSignup,
|
public async Task CreateOrganizationAsync_Success(Provider provider, OrganizationSignup organizationSignup,
|
||||||
Organization organization, string clientOwnerEmail, User user, SutProvider<ProviderService> sutProvider)
|
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);
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup)
|
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
||||||
.Returns((organization, null as OrganizationUser, new Collection()));
|
.Returns((organization, null as OrganizationUser, new Collection()));
|
||||||
|
|
||||||
var providerOrganization =
|
var providerOrganization =
|
||||||
@ -688,7 +656,7 @@ public class ProviderServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, OrganizationCustomize, BitAutoData]
|
[Theory, OrganizationCustomize, BitAutoData]
|
||||||
public async Task CreateOrganizationAsync_ConsolidatedBillingEnabled_InvalidPlanType_ThrowsBadRequestException(
|
public async Task CreateOrganizationAsync_InvalidPlanType_ThrowsBadRequestException(
|
||||||
Provider provider,
|
Provider provider,
|
||||||
OrganizationSignup organizationSignup,
|
OrganizationSignup organizationSignup,
|
||||||
Organization organization,
|
Organization organization,
|
||||||
@ -696,8 +664,6 @@ public class ProviderServiceTests
|
|||||||
User user,
|
User user,
|
||||||
SutProvider<ProviderService> sutProvider)
|
SutProvider<ProviderService> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
|
||||||
|
|
||||||
provider.Type = ProviderType.Msp;
|
provider.Type = ProviderType.Msp;
|
||||||
provider.Status = ProviderStatusType.Billable;
|
provider.Status = ProviderStatusType.Billable;
|
||||||
|
|
||||||
@ -717,7 +683,7 @@ public class ProviderServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, OrganizationCustomize, BitAutoData]
|
[Theory, OrganizationCustomize, BitAutoData]
|
||||||
public async Task CreateOrganizationAsync_ConsolidatedBillingEnabled_InvokeSignupClientAsync(
|
public async Task CreateOrganizationAsync_InvokeSignupClientAsync(
|
||||||
Provider provider,
|
Provider provider,
|
||||||
OrganizationSignup organizationSignup,
|
OrganizationSignup organizationSignup,
|
||||||
Organization organization,
|
Organization organization,
|
||||||
@ -725,8 +691,6 @@ public class ProviderServiceTests
|
|||||||
User user,
|
User user,
|
||||||
SutProvider<ProviderService> sutProvider)
|
SutProvider<ProviderService> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
|
||||||
|
|
||||||
provider.Type = ProviderType.Msp;
|
provider.Type = ProviderType.Msp;
|
||||||
provider.Status = ProviderStatusType.Billable;
|
provider.Status = ProviderStatusType.Billable;
|
||||||
|
|
||||||
@ -771,11 +735,11 @@ public class ProviderServiceTests
|
|||||||
(Provider provider, OrganizationSignup organizationSignup, Organization organization, string clientOwnerEmail,
|
(Provider provider, OrganizationSignup organizationSignup, Organization organization, string clientOwnerEmail,
|
||||||
User user, SutProvider<ProviderService> sutProvider, Collection defaultCollection)
|
User user, SutProvider<ProviderService> sutProvider, Collection defaultCollection)
|
||||||
{
|
{
|
||||||
organizationSignup.Plan = PlanType.EnterpriseAnnually;
|
organizationSignup.Plan = PlanType.EnterpriseMonthly;
|
||||||
|
|
||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup)
|
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
||||||
.Returns((organization, null as OrganizationUser, defaultCollection));
|
.Returns((organization, null as OrganizationUser, defaultCollection));
|
||||||
|
|
||||||
var providerOrganization =
|
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.Admin.Utilities;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -53,8 +55,8 @@ public class OrganizationsController : Controller
|
|||||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||||
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
|
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
|
||||||
private readonly IFeatureService _featureService;
|
|
||||||
private readonly IProviderBillingService _providerBillingService;
|
private readonly IProviderBillingService _providerBillingService;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
public OrganizationsController(
|
public OrganizationsController(
|
||||||
IOrganizationService organizationService,
|
IOrganizationService organizationService,
|
||||||
@ -80,8 +82,8 @@ public class OrganizationsController : Controller
|
|||||||
IServiceAccountRepository serviceAccountRepository,
|
IServiceAccountRepository serviceAccountRepository,
|
||||||
IProviderOrganizationRepository providerOrganizationRepository,
|
IProviderOrganizationRepository providerOrganizationRepository,
|
||||||
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
||||||
IFeatureService featureService,
|
IProviderBillingService providerBillingService,
|
||||||
IProviderBillingService providerBillingService)
|
IFeatureService featureService)
|
||||||
{
|
{
|
||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
@ -106,8 +108,8 @@ public class OrganizationsController : Controller
|
|||||||
_serviceAccountRepository = serviceAccountRepository;
|
_serviceAccountRepository = serviceAccountRepository;
|
||||||
_providerOrganizationRepository = providerOrganizationRepository;
|
_providerOrganizationRepository = providerOrganizationRepository;
|
||||||
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
|
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
|
||||||
_featureService = featureService;
|
|
||||||
_providerBillingService = providerBillingService;
|
_providerBillingService = providerBillingService;
|
||||||
|
_featureService = featureService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequirePermission(Permission.Org_List_View)]
|
[RequirePermission(Permission.Org_List_View)]
|
||||||
@ -230,7 +232,23 @@ public class OrganizationsController : Controller
|
|||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<IActionResult> Edit(Guid id, OrganizationEditModel model)
|
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 &&
|
if (organization.UseSecretsManager &&
|
||||||
!StaticStore.GetPlan(organization.PlanType).SupportsSecretsManager)
|
!StaticStore.GetPlan(organization.PlanType).SupportsSecretsManager)
|
||||||
@ -239,7 +257,12 @@ public class OrganizationsController : Controller
|
|||||||
return RedirectToAction("Edit", new { id });
|
return RedirectToAction("Edit", new { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await HandlePotentialProviderSeatScalingAsync(
|
||||||
|
existingOrganizationData,
|
||||||
|
model);
|
||||||
|
|
||||||
await _organizationRepository.ReplaceAsync(organization);
|
await _organizationRepository.ReplaceAsync(organization);
|
||||||
|
|
||||||
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
||||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.OrganizationEditedByAdmin, organization, _currentContext)
|
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.OrganizationEditedByAdmin, organization, _currentContext)
|
||||||
{
|
{
|
||||||
@ -262,9 +285,7 @@ public class OrganizationsController : Controller
|
|||||||
return RedirectToAction("Index");
|
return RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
||||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
if (organization.IsValidClient())
|
||||||
|
|
||||||
if (consolidatedBillingEnabled && organization.IsValidClient())
|
|
||||||
{
|
{
|
||||||
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||||
|
|
||||||
@ -394,10 +415,9 @@ public class OrganizationsController : Controller
|
|||||||
|
|
||||||
return Json(null);
|
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))
|
if (_accessControlService.UserHasPermission(Permission.Org_CheckEnabledBox))
|
||||||
{
|
{
|
||||||
organization.Enabled = model.Enabled;
|
organization.Enabled = model.Enabled;
|
||||||
@ -449,7 +469,62 @@ public class OrganizationsController : Controller
|
|||||||
organization.GatewayCustomerId = model.GatewayCustomerId;
|
organization.GatewayCustomerId = model.GatewayCustomerId;
|
||||||
organization.GatewaySubscriptionId = model.GatewaySubscriptionId;
|
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 _providerRepository.ReplaceAsync(provider);
|
||||||
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
|
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
|
||||||
|
|
||||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
if (!provider.IsBillable())
|
||||||
|
|
||||||
if (!isConsolidatedBillingEnabled || !provider.IsBillable())
|
|
||||||
{
|
{
|
||||||
return RedirectToAction("Edit", new { id });
|
return RedirectToAction("Edit", new { id });
|
||||||
}
|
}
|
||||||
@ -340,10 +338,7 @@ public class ProvidersController : Controller
|
|||||||
var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);
|
var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);
|
||||||
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
|
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
|
||||||
|
|
||||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
if (!provider.IsBillable())
|
||||||
|
|
||||||
|
|
||||||
if (!isConsolidatedBillingEnabled || !provider.IsBillable())
|
|
||||||
{
|
{
|
||||||
return new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>());
|
return new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>());
|
||||||
}
|
}
|
||||||
|
@ -103,19 +103,19 @@
|
|||||||
|
|
||||||
<div class="d-flex mt-4">
|
<div class="d-flex mt-4">
|
||||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
<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)
|
@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
|
Teams Trial
|
||||||
</button>
|
</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
|
Enterprise Trial
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (canUnlinkFromProvider && Model.Provider is not null)
|
@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');">
|
onclick="return unlinkProvider('@Model.Organization.Id');">
|
||||||
Unlink provider
|
Unlink provider
|
||||||
</button>
|
</button>
|
||||||
@ -124,7 +124,7 @@
|
|||||||
{
|
{
|
||||||
<form asp-action="DeleteInitiation" asp-route-id="@Model.Organization.Id" id="initiate-delete-form">
|
<form asp-action="DeleteInitiation" asp-route-id="@Model.Organization.Id" id="initiate-delete-form">
|
||||||
<input type="hidden" name="AdminEmail" id="AdminEmail" />
|
<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>
|
||||||
<form asp-action="Delete" asp-route-id="@Model.Organization.Id"
|
<form asp-action="Delete" asp-route-id="@Model.Organization.Id"
|
||||||
onsubmit="return confirm('Are you sure you want to hard delete this organization?')">
|
onsubmit="return confirm('Are you sure you want to hard delete this organization?')">
|
||||||
|
@ -5,21 +5,31 @@
|
|||||||
|
|
||||||
<h1>Organizations</h1>
|
<h1>Organizations</h1>
|
||||||
|
|
||||||
<form class="form-inline mb-2" method="get">
|
<form class="row row-cols-lg-auto g-3 align-items-center mb-2" method="get">
|
||||||
<label class="sr-only" asp-for="Name">Name</label>
|
<div class="col-12">
|
||||||
<input type="text" class="form-control mb-2 mr-2" placeholder="Name" asp-for="Name" name="name">
|
<label class="visually-hidden" asp-for="Name">Name</label>
|
||||||
<label class="sr-only" asp-for="UserEmail">User email</label>
|
<input type="text" class="form-control" placeholder="Name" asp-for="Name" name="name">
|
||||||
<input type="text" class="form-control mb-2 mr-2" placeholder="User email" asp-for="UserEmail" name="userEmail">
|
</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)
|
@if(!Model.SelfHosted)
|
||||||
{
|
{
|
||||||
<label class="sr-only" asp-for="Paid">Customer</label>
|
<div class="col-12">
|
||||||
<select class="form-control mb-2 mr-2" asp-for="Paid" name="paid">
|
<label class="visually-hidden" asp-for="Paid">Customer</label>
|
||||||
<option asp-selected="!Model.Paid.HasValue" value="">-- Customer --</option>
|
<select class="form-select" asp-for="Paid" name="paid">
|
||||||
<option asp-selected="Model.Paid.GetValueOrDefault(false)" value="true">Paid</option>
|
<option asp-selected="!Model.Paid.HasValue" value="">-- Customer --</option>
|
||||||
<option asp-selected="!Model.Paid.GetValueOrDefault(true)" value="false">Freeloader</option>
|
<option asp-selected="Model.Paid.GetValueOrDefault(false)" value="true">Paid</option>
|
||||||
</select>
|
<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>
|
</form>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@ -68,7 +78,7 @@
|
|||||||
}
|
}
|
||||||
else
|
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)
|
@if(org.MaxStorageGb.HasValue && org.MaxStorageGb > 1)
|
||||||
@ -78,7 +88,7 @@
|
|||||||
}
|
}
|
||||||
else
|
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>
|
title="No Additional Storage"></i>
|
||||||
}
|
}
|
||||||
@if(org.Enabled)
|
@if(org.Enabled)
|
||||||
@ -88,7 +98,7 @@
|
|||||||
}
|
}
|
||||||
else
|
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())
|
@if(org.TwoFactorIsEnabled())
|
||||||
{
|
{
|
||||||
@ -96,7 +106,7 @@
|
|||||||
}
|
}
|
||||||
else
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -9,12 +9,18 @@
|
|||||||
<h1>Add Existing Organization</h1>
|
<h1>Add Existing Organization</h1>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<form class="form-inline mb-2" method="get" asp-route-id="@providerId">
|
<form class="row g-3 align-items-center mb-2" method="get" asp-route-id="@providerId">
|
||||||
<label class="sr-only" asp-for="OrganizationName"></label>
|
<div class="col">
|
||||||
<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="visually-hidden" asp-for="OrganizationName"></label>
|
||||||
<label class="sr-only" asp-for="OrganizationOwnerEmail"></label>
|
<input type="text" class="form-control" placeholder="@Html.DisplayNameFor(m => m.OrganizationName)" asp-for="OrganizationName" name="name">
|
||||||
<input type="email" class="form-control mb-2 mr-2 flex-fill" placeholder="@Html.DisplayNameFor(m => m.OrganizationOwnerEmail)" asp-for="OrganizationOwnerEmail" name="ownerEmail">
|
</div>
|
||||||
<button type="submit" class="btn btn-primary mb-2" title="Search" formmethod="get"><i class="fa fa-search"></i> Search</button>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,23 +22,23 @@
|
|||||||
<h1>Create Provider</h1>
|
<h1>Create Provider</h1>
|
||||||
<form method="post" asp-action="Create">
|
<form method="post" asp-action="Create">
|
||||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="Type" class="h2"></label>
|
<label asp-for="Type" class="form-label h2"></label>
|
||||||
@foreach (var providerType in providerTypes)
|
@foreach (var providerType in providerTypes)
|
||||||
{
|
{
|
||||||
var providerTypeValue = (int)providerType;
|
var providerTypeValue = (int)providerType;
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
@Html.RadioButtonFor(m => m.Type, providerType, new { id = $"providerType-{providerTypeValue}", @class = "form-check-input" })
|
@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>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,39 +1,31 @@
|
|||||||
@using Bit.Core.AdminConsole.Enums.Provider
|
|
||||||
@using Bit.Core
|
|
||||||
|
|
||||||
@model CreateMspProviderModel
|
@model CreateMspProviderModel
|
||||||
|
|
||||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Create Managed Service Provider";
|
ViewData["Title"] = "Create Managed Service Provider";
|
||||||
}
|
}
|
||||||
|
|
||||||
<h1>Create Managed Service Provider</h1>
|
<h1>Create Managed Service Provider</h1>
|
||||||
<div>
|
<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 asp-validation-summary="All" class="alert alert-danger"></div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="OwnerEmail"></label>
|
<label asp-for="OwnerEmail" class="form-label"></label>
|
||||||
<input type="text" class="form-control" asp-for="OwnerEmail">
|
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||||
</div>
|
</div>
|
||||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
|
||||||
{
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
<label asp-for="TeamsMonthlySeatMinimum" class="form-label"></label>
|
||||||
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
<label asp-for="EnterpriseMonthlySeatMinimum" class="form-label"></label>
|
||||||
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,17 +7,17 @@
|
|||||||
ViewData["Title"] = "Create Multi-organization Enterprise Provider";
|
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>
|
<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 asp-validation-summary="All" class="alert alert-danger"></div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="OwnerEmail"></label>
|
<label asp-for="OwnerEmail" class="form-label"></label>
|
||||||
<input type="text" class="form-control" asp-for="OwnerEmail">
|
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
@{
|
@{
|
||||||
var multiOrgPlans = new List<PlanType>
|
var multiOrgPlans = new List<PlanType>
|
||||||
{
|
{
|
||||||
@ -25,19 +25,19 @@
|
|||||||
PlanType.EnterpriseMonthly
|
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)">
|
<select class="form-select" asp-for="Plan" asp-items="Html.GetEnumSelectList(multiOrgPlans)">
|
||||||
<option value="">--</option>
|
<option value="">--</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="EnterpriseSeatMinimum"></label>
|
<label asp-for="EnterpriseSeatMinimum" class="form-label"></label>
|
||||||
<input type="number" class="form-control" asp-for="EnterpriseSeatMinimum">
|
<input type="number" class="form-control" asp-for="EnterpriseSeatMinimum">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
@await Html.PartialAsync("~/AdminConsole/Views/Shared/_OrganizationForm.cshtml", Model)
|
@await Html.PartialAsync("~/AdminConsole/Views/Shared/_OrganizationForm.cshtml", Model)
|
||||||
<div class="d-flex mt-4">
|
<div class="d-flex mt-4">
|
||||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
<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"
|
<form asp-controller="Providers" asp-action="Edit" asp-route-id="@Model.Provider.Id"
|
||||||
onsubmit="return confirm('Are you sure you want to cancel?')">
|
onsubmit="return confirm('Are you sure you want to cancel?')">
|
||||||
<button class="btn btn-outline-secondary" type="submit">Cancel</button>
|
<button class="btn btn-outline-secondary" type="submit">Cancel</button>
|
||||||
|
@ -6,18 +6,18 @@
|
|||||||
|
|
||||||
<h1>Create Reseller Provider</h1>
|
<h1>Create Reseller Provider</h1>
|
||||||
<div>
|
<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 asp-validation-summary="All" class="alert alert-danger"></div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="Name"></label>
|
<label asp-for="Name" class="form-label"></label>
|
||||||
<input type="text" class="form-control" asp-for="Name">
|
<input type="text" class="form-control" asp-for="Name">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="BusinessName"></label>
|
<label asp-for="BusinessName" class="form-label"></label>
|
||||||
<input type="text" class="form-control" asp-for="BusinessName">
|
<input type="text" class="form-control" asp-for="BusinessName">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="BillingEmail"></label>
|
<label asp-for="BillingEmail" class="form-label"></label>
|
||||||
<input type="text" class="form-control" asp-for="BillingEmail">
|
<input type="text" class="form-control" asp-for="BillingEmail">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||||
|
@ -34,21 +34,21 @@
|
|||||||
<h2>Billing</h2>
|
<h2>Billing</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="BillingEmail"></label>
|
<label asp-for="BillingEmail" class="form-label"></label>
|
||||||
<input type="email" class="form-control" asp-for="BillingEmail" readonly='@(!canEdit)'>
|
<input type="email" class="form-control" asp-for="BillingEmail" readonly='@(!canEdit)'>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="BillingPhone"></label>
|
<label asp-for="BillingPhone" class="form-label"></label>
|
||||||
<input type="tel" class="form-control" asp-for="BillingPhone">
|
<input type="tel" class="form-control" asp-for="BillingPhone">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && Model.Provider.IsBillable())
|
@if (Model.Provider.IsBillable())
|
||||||
{
|
{
|
||||||
switch (Model.Provider.Type)
|
switch (Model.Provider.Type)
|
||||||
{
|
{
|
||||||
@ -56,58 +56,18 @@
|
|||||||
{
|
{
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
<label asp-for="TeamsMonthlySeatMinimum" class="form-label"></label>
|
||||||
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
<label asp-for="EnterpriseMonthlySeatMinimum" class="form-label"></label>
|
||||||
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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;
|
break;
|
||||||
}
|
}
|
||||||
case ProviderType.MultiOrganizationEnterprise:
|
case ProviderType.MultiOrganizationEnterprise:
|
||||||
@ -116,7 +76,7 @@
|
|||||||
{
|
{
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
@{
|
@{
|
||||||
var multiOrgPlans = new List<PlanType>
|
var multiOrgPlans = new List<PlanType>
|
||||||
{
|
{
|
||||||
@ -124,15 +84,15 @@
|
|||||||
PlanType.EnterpriseMonthly
|
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)">
|
<select class="form-control" asp-for="Plan" asp-items="Html.GetEnumSelectList(multiOrgPlans)">
|
||||||
<option value="">--</option>
|
<option value="">--</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="EnterpriseMinimumSeats"></label>
|
<label asp-for="EnterpriseMinimumSeats" class="form-label"></label>
|
||||||
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
|
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -141,6 +101,40 @@
|
|||||||
break;
|
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>
|
</form>
|
||||||
@await Html.PartialAsync("Organizations", Model)
|
@await Html.PartialAsync("Organizations", Model)
|
||||||
@ -151,21 +145,21 @@
|
|||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content rounded">
|
<div class="modal-content rounded">
|
||||||
<div class="p-3">
|
<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>
|
||||||
<div class="modal-body">
|
<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.
|
Enter the email of the provider admin that will receive the request to delete the provider portal.
|
||||||
</span>
|
</span>
|
||||||
<form>
|
<form>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="provider-email" class="col-form-label">Provider email</label>
|
<label for="provider-email" class="col-form-label">Provider email</label>
|
||||||
<input type="email" class="form-control" id="provider-email">
|
<input type="email" class="form-control" id="provider-email">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<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>
|
<button type="button" class="btn btn-danger btn-pill" onclick="initiateDeleteProvider('@Model.Provider.Id')">Send email request</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -175,21 +169,21 @@
|
|||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content rounded">
|
<div class="modal-content rounded">
|
||||||
<div class="p-3">
|
<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>
|
||||||
<div class="modal-body">
|
<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.
|
This action is permanent and irreversible. Enter the provider name to complete deletion of the provider and associated data.
|
||||||
</span>
|
</span>
|
||||||
<form>
|
<form>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="provider-name" class="col-form-label">Provider name</label>
|
<label for="provider-name" class="col-form-label">Provider name</label>
|
||||||
<input type="text" class="form-control" id="provider-name">
|
<input type="text" class="form-control" id="provider-name">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<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>
|
<button type="button" class="btn btn-danger btn-pill" onclick="deleteProvider('@Model.Provider.Id');">Delete provider</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -199,12 +193,12 @@
|
|||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<div class="modal-content rounded">
|
<div class="modal-content rounded">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<h4 class="font-weight-bolder">Cannot Delete @Model.Name</h4>
|
<h4 class="fw-bolder">Cannot Delete @Model.Name</h4>
|
||||||
<p class="font-weight-lighter">You must unlink all clients before you can delete @Model.Name.</p>
|
<p class="fw-lighter">You must unlink all clients before you can delete @Model.Name.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<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-primary btn-pill" data-dismiss="modal">Ok</button>
|
<button type="button" class="btn btn-primary btn-pill" data-bs-dismiss="modal">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -214,15 +208,14 @@
|
|||||||
|
|
||||||
<div class="d-flex mt-4">
|
<div class="d-flex mt-4">
|
||||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
<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 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 class="btn btn-outline-danger ms-2" onclick="openDeleteModal(@Model.ProviderOrganizations.Count())">Delete</button>
|
||||||
<button id="deleteBtn" hidden="hidden" data-toggle="modal" data-target="#DeleteModal"></button>
|
<button id="deleteBtn" hidden="hidden" data-bs-toggle="modal" data-bs-target="#DeleteModal"></button>
|
||||||
|
|
||||||
<button id="linkAccWarningBtn" hidden="hidden" data-toggle="modal" data-target="#linkedWarningModal"></button>
|
|
||||||
|
|
||||||
|
<button id="linkAccWarningBtn" hidden="hidden" data-bs-toggle="modal" data-bs-target="#linkedWarningModal"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -11,23 +11,27 @@
|
|||||||
|
|
||||||
<h1>Providers</h1>
|
<h1>Providers</h1>
|
||||||
|
|
||||||
<div class="row mb-2">
|
<form class="row row-cols-lg-auto g-3 align-items-center mb-2" method="get">
|
||||||
<div class="col">
|
<div class="col-12">
|
||||||
<form class="form-inline mb-2" method="get">
|
<label class="visually-hidden" asp-for="Name">Name</label>
|
||||||
<label class="sr-only" asp-for="Name">Name</label>
|
<input type="text" class="form-control" placeholder="Name" asp-for="Name" name="name">
|
||||||
<input type="text" class="form-control mb-2 mr-2" placeholder="Name" asp-for="Name" name="name">
|
</div>
|
||||||
<label class="sr-only" asp-for="UserEmail">User email</label>
|
<div class="col-12">
|
||||||
<input type="text" class="form-control mb-2 mr-2" placeholder="User email" asp-for="UserEmail" name="userEmail">
|
<label class="visually-hidden" asp-for="UserEmail">User email</label>
|
||||||
<button type="submit" class="btn btn-primary mb-2" title="Search"><i class="fa fa-search"></i> Search</button>
|
<input type="text" class="form-control" placeholder="User email" asp-for="UserEmail" name="userEmail">
|
||||||
</form>
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<button type="submit" class="btn btn-primary" title="Search">
|
||||||
|
<i class="fa fa-search"></i> Search
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@if (canCreateProvider)
|
@if (canCreateProvider)
|
||||||
{
|
{
|
||||||
<div class="col-auto">
|
<div class="col-auto ms-auto">
|
||||||
<a asp-action="Create" class="btn btn-secondary">Create Provider</a>
|
<a asp-action="Create" class="btn btn-secondary">Create Provider</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</form>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-hover">
|
<table class="table table-striped table-hover">
|
||||||
|
@ -24,9 +24,9 @@
|
|||||||
<th>
|
<th>
|
||||||
@if (Model.Provider.Type == ProviderType.Reseller)
|
@if (Model.Provider.Type == ProviderType.Reseller)
|
||||||
{
|
{
|
||||||
<div class="float-right text-nowrap">
|
<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">New Organization</a>
|
<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">Add Existing 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>
|
</div>
|
||||||
}
|
}
|
||||||
</th>
|
</th>
|
||||||
@ -51,16 +51,16 @@
|
|||||||
@providerOrganization.Status
|
@providerOrganization.Status
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="float-right">
|
<div class="float-end">
|
||||||
@if (canUnlinkFromProvider)
|
@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
|
Unlink provider
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
@if (providerOrganization.Status == OrganizationStatusType.Pending)
|
@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
|
Resend invitation
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,8 @@
|
|||||||
<h2>General</h2>
|
<h2>General</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="Name"></label>
|
<label class="form-label" asp-for="Name"></label>
|
||||||
<input type="text" class="form-control" asp-for="Name" value="@Model.Name" required>
|
<input type="text" class="form-control" asp-for="Name" value="@Model.Name" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -37,17 +37,17 @@
|
|||||||
{
|
{
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label>Client Owner Email</label>
|
<label class="form-label">Client Owner Email</label>
|
||||||
@if (!string.IsNullOrWhiteSpace(Model.Owners))
|
@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
|
else
|
||||||
{
|
{
|
||||||
<input type="text" class="form-control" asp-for="Owners" required>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -66,8 +66,8 @@
|
|||||||
<h2>Plan</h2>
|
<h2>Plan</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="PlanType"></label>
|
<label class="form-label" asp-for="PlanType"></label>
|
||||||
@{
|
@{
|
||||||
var planTypes = Enum.GetValues<PlanType>()
|
var planTypes = Enum.GetValues<PlanType>()
|
||||||
.Where(p =>
|
.Where(p =>
|
||||||
@ -83,12 +83,12 @@
|
|||||||
})
|
})
|
||||||
.ToList();
|
.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>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="Plan"></label>
|
<label class="form-label" asp-for="Plan"></label>
|
||||||
<input type="text" class="form-control" asp-for="Plan" required readonly='@(!canEditPlan)'>
|
<input type="text" class="form-control" asp-for="Plan" required readonly='@(!canEditPlan)'>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -172,28 +172,28 @@
|
|||||||
<h2>Password Manager Configuration</h2>
|
<h2>Password Manager Configuration</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="Seats"></label>
|
<label class="form-label" asp-for="Seats"></label>
|
||||||
<input type="number" class="form-control" asp-for="Seats" min="1" readonly='@(!canEditPlan)'>
|
<input type="number" class="form-control" asp-for="Seats" min="1" readonly='@(!canEditPlan)'>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="MaxCollections"></label>
|
<label class="form-label" asp-for="MaxCollections"></label>
|
||||||
<input type="number" class="form-control" asp-for="MaxCollections" min="1" readonly='@(!canEditPlan)'>
|
<input type="number" class="form-control" asp-for="MaxCollections" min="1" readonly='@(!canEditPlan)'>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="MaxStorageGb"></label>
|
<label class="form-label" asp-for="MaxStorageGb"></label>
|
||||||
<input type="number" class="form-control" asp-for="MaxStorageGb" min="1" readonly='@(!canEditPlan)'>
|
<input type="number" class="form-control" asp-for="MaxStorageGb" min="1" readonly='@(!canEditPlan)'>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="MaxAutoscaleSeats"></label>
|
<label class="form-label" asp-for="MaxAutoscaleSeats"></label>
|
||||||
<input type="number" class="form-control" asp-for="MaxAutoscaleSeats" min="1" readonly='@(!canEditPlan)'>
|
<input type="number" class="form-control" asp-for="MaxAutoscaleSeats" min="1" readonly='@(!canEditPlan)'>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -202,32 +202,32 @@
|
|||||||
|
|
||||||
@if (canViewPlan)
|
@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>
|
<h2>Secrets Manager Configuration</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="SmSeats"></label>
|
<label class="form-label" asp-for="SmSeats"></label>
|
||||||
<input type="number" class="form-control" asp-for="SmSeats" min="1" readonly='@(!canEditPlan)'>
|
<input type="number" class="form-control" asp-for="SmSeats" min="1" readonly='@(!canEditPlan)'>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="MaxAutoscaleSmSeats"></label>
|
<label class="form-label" asp-for="MaxAutoscaleSmSeats"></label>
|
||||||
<input type="number" class="form-control" asp-for="MaxAutoscaleSmSeats" min="1" readonly='@(!canEditPlan)'>
|
<input type="number" class="form-control" asp-for="MaxAutoscaleSmSeats" min="1" readonly='@(!canEditPlan)'>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="SmServiceAccounts"></label>
|
<label class="form-label" asp-for="SmServiceAccounts"></label>
|
||||||
<input type="number" class="form-control" asp-for="SmServiceAccounts" min="1" readonly='@(!canEditPlan)'>
|
<input type="number" class="form-control" asp-for="SmServiceAccounts" min="1" readonly='@(!canEditPlan)'>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="MaxAutoscaleSmServiceAccounts"></label>
|
<label class="form-label" asp-for="MaxAutoscaleSmServiceAccounts"></label>
|
||||||
<input type="number" class="form-control" asp-for="MaxAutoscaleSmServiceAccounts" min="1" readonly='@(!canEditPlan)'>
|
<input type="number" class="form-control" asp-for="MaxAutoscaleSmServiceAccounts" min="1" readonly='@(!canEditPlan)'>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -240,14 +240,14 @@
|
|||||||
<h2>Licensing</h2>
|
<h2>Licensing</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="LicenseKey"></label>
|
<label class="form-label" asp-for="LicenseKey"></label>
|
||||||
<input type="text" class="form-control" asp-for="LicenseKey" readonly='@(!canEditLicensing)'>
|
<input type="text" class="form-control" asp-for="LicenseKey" readonly='@(!canEditLicensing)'>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="ExpirationDate"></label>
|
<label class="form-label" asp-for="ExpirationDate"></label>
|
||||||
<input type="datetime-local" class="form-control" asp-for="ExpirationDate" readonly='@(!canEditLicensing)' step="1">
|
<input type="datetime-local" class="form-control" asp-for="ExpirationDate" readonly='@(!canEditLicensing)' step="1">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -259,52 +259,46 @@
|
|||||||
<h2>Billing</h2>
|
<h2>Billing</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="BillingEmail"></label>
|
<label class="form-label" asp-for="BillingEmail"></label>
|
||||||
<input type="email" class="form-control" asp-for="BillingEmail" readonly="readonly">
|
<input type="email" class="form-control" asp-for="BillingEmail" readonly="readonly">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<div class="form-group">
|
<label class="form-label" asp-for="Gateway"></label>
|
||||||
<label asp-for="Gateway"></label>
|
<select class="form-select" asp-for="Gateway" disabled="@(!canEditBilling)"
|
||||||
<select class="form-control" asp-for="Gateway" disabled='@(canEditBilling ? null : "disabled")'
|
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||||
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
<option value="">--</option>
|
||||||
<option value="">--</option>
|
</select>
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="GatewayCustomerId"></label>
|
<label class="form-label" asp-for="GatewayCustomerId"></label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" asp-for="GatewayCustomerId" readonly='@(!canEditBilling)'>
|
<input type="text" class="form-control" asp-for="GatewayCustomerId" readonly='@(!canEditBilling)'>
|
||||||
@if(canLaunchGateway)
|
@if(canLaunchGateway)
|
||||||
{
|
{
|
||||||
<div class="input-group-append">
|
<button class="btn btn-secondary" type="button" id="gateway-customer-link">
|
||||||
<button class="btn btn-secondary" type="button" id="gateway-customer-link">
|
<i class="fa fa-external-link"></i>
|
||||||
<i class="fa fa-external-link"></i>
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="GatewaySubscriptionId"></label>
|
<label class="form-label" asp-for="GatewaySubscriptionId"></label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" asp-for="GatewaySubscriptionId" readonly='@(!canEditBilling)'>
|
<input type="text" class="form-control" asp-for="GatewaySubscriptionId" readonly='@(!canEditBilling)'>
|
||||||
@if (canLaunchGateway)
|
@if (canLaunchGateway)
|
||||||
{
|
{
|
||||||
<div class="input-group-append">
|
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
|
||||||
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
|
<i class="fa fa-external-link"></i>
|
||||||
<i class="fa fa-external-link"></i>
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
ViewData["Title"] = "Login";
|
ViewData["Title"] = "Login";
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="row justify-content-md-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col col-lg-6 col-md-8">
|
<div class="col-lg-6 col-md-8">
|
||||||
@if(!string.IsNullOrWhiteSpace(Model.Success))
|
@if(!string.IsNullOrWhiteSpace(Model.Success))
|
||||||
{
|
{
|
||||||
<div class="alert alert-success" role="alert">@Model.Success</div>
|
<div class="alert alert-success" role="alert">@Model.Success</div>
|
||||||
@ -19,12 +19,12 @@
|
|||||||
<form asp-action="" method="post">
|
<form asp-action="" method="post">
|
||||||
<input type="hidden" asp-for="ReturnUrl" />
|
<input type="hidden" asp-for="ReturnUrl" />
|
||||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="Email" class="sr-only">Email Address</label>
|
<label asp-for="Email" class="visually-hidden">Email Address</label>
|
||||||
<input asp-for="Email" type="email" class="form-control" placeholder="ex. john@example.com"
|
<input asp-for="Email" type="email" class="form-control" placeholder="ex. john@example.com"
|
||||||
required autofocus>
|
required autofocus>
|
||||||
<span asp-validation-for="Email" class="invalid-feedback"></span>
|
<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>
|
</div>
|
||||||
<button class="btn btn-primary" type="submit">Continue</button>
|
<button class="btn btn-primary" type="submit">Continue</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -25,22 +25,22 @@ bf82d3cf-0e21-4f39-b81b-ef52b2fc6a3a
|
|||||||
</div>
|
</div>
|
||||||
<form method="post" asp-controller="MigrateProviders" asp-action="Post" class="mt-2">
|
<form method="post" asp-controller="MigrateProviders" asp-action="Post" class="mt-2">
|
||||||
<div asp-validation-summary="All" class="text-danger"></div>
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="ProviderIds"></label>
|
<label class="form-label" asp-for="ProviderIds"></label>
|
||||||
<textarea rows="10" class="form-control" type="text" asp-for="ProviderIds"></textarea>
|
<textarea rows="10" class="form-control" type="text" asp-for="ProviderIds"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<input type="submit" value="Run" class="btn btn-primary mb-2"/>
|
<input type="submit" value="Run" class="btn btn-primary"/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<form method="get" asp-controller="MigrateProviders" asp-action="Results" class="mt-2">
|
<form method="get" asp-controller="MigrateProviders" asp-action="Results" class="mt-2">
|
||||||
<div asp-validation-summary="All" class="text-danger"></div>
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="ProviderIds"></label>
|
<label class="form-label" asp-for="ProviderIds"></label>
|
||||||
<textarea rows="10" class="form-control" type="text" asp-for="ProviderIds"></textarea>
|
<textarea rows="10" class="form-control" type="text" asp-for="ProviderIds"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<input type="submit" value="See Previous Results" class="btn btn-primary mb-2"/>
|
<input type="submit" value="See Previous Results" class="btn btn-primary"/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
@import "webfonts.scss";
|
@import "webfonts.scss";
|
||||||
|
@import "bootstrap/scss/functions";
|
||||||
|
@import "bootstrap/scss/variables";
|
||||||
|
@import "bootstrap/scss/mixins";
|
||||||
|
|
||||||
$primary: #175DDC;
|
$primary: #175DDC;
|
||||||
$primary-accent: #1252A3;
|
$primary-accent: #1252A3;
|
||||||
@ -7,7 +10,8 @@ $info: #555555;
|
|||||||
$warning: #bf7e16;
|
$warning: #bf7e16;
|
||||||
$danger: #dd4b39;
|
$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';
|
$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;
|
$h1-font-size: 2rem;
|
||||||
@ -17,7 +21,7 @@ $h4-font-size: 1rem;
|
|||||||
$h5-font-size: 1rem;
|
$h5-font-size: 1rem;
|
||||||
$h6-font-size: 1rem;
|
$h6-font-size: 1rem;
|
||||||
|
|
||||||
@import "bootstrap/scss/bootstrap.scss";
|
@import "bootstrap/scss/bootstrap";
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
border-bottom: 1px solid $border-color;
|
border-bottom: 1px solid $border-color;
|
||||||
@ -49,3 +53,11 @@ h3 {
|
|||||||
.form-check-input {
|
.form-check-input {
|
||||||
margin-top: .45rem;
|
margin-top: .45rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -105,7 +105,7 @@
|
|||||||
<h3>SMTP</h3>
|
<h3>SMTP</h3>
|
||||||
@if(!Bit.Core.Utilities.CoreHelpers.SettingHasValue(Model.GlobalSettings.Mail?.Smtp?.Host))
|
@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
|
else
|
||||||
{
|
{
|
||||||
@ -159,7 +159,7 @@ else
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="text-muted">Not configured</span>
|
<span class="text-body-secondary">Not configured</span>
|
||||||
}
|
}
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
@ -171,7 +171,7 @@ else
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="text-muted">Not configured</span>
|
<span class="text-body-secondary">Not configured</span>
|
||||||
}
|
}
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
@ -183,7 +183,7 @@ else
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="text-muted">Not configured</span>
|
<span class="text-body-secondary">Not configured</span>
|
||||||
}
|
}
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
@ -37,12 +37,12 @@
|
|||||||
<a class="navbar-brand" asp-controller="Home" asp-action="Index">
|
<a class="navbar-brand" asp-controller="Home" asp-action="Index">
|
||||||
<i class="fa fa-lg fa-fw fa-shield"></i> Admin
|
<i class="fa fa-lg fa-fw fa-shield"></i> Admin
|
||||||
</a>
|
</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">
|
aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
<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 (SignInManager.IsSignedIn(User))
|
||||||
{
|
{
|
||||||
@if (canViewUsers)
|
@if (canViewUsers)
|
||||||
@ -69,10 +69,10 @@
|
|||||||
{
|
{
|
||||||
<li class="nav-item dropdown" active-controller="tools">
|
<li class="nav-item dropdown" active-controller="tools">
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="toolsDropdown" role="button"
|
<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
|
Tools
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" aria-labelledby="toolsDropdown">
|
<ul class="dropdown-menu" aria-labelledby="toolsDropdown">
|
||||||
@if (canChargeBraintree)
|
@if (canChargeBraintree)
|
||||||
{
|
{
|
||||||
<a class="dropdown-item" asp-controller="Tools" asp-action="ChargeBraintree">
|
<a class="dropdown-item" asp-controller="Tools" asp-action="ChargeBraintree">
|
||||||
@ -121,7 +121,7 @@
|
|||||||
Migrate Providers
|
Migrate Providers
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</div>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,30 +10,30 @@
|
|||||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="UserId"></label>
|
<label asp-for="UserId" class="form-label"></label>
|
||||||
<input type="text" class="form-control" asp-for="UserId">
|
<input type="text" class="form-control" asp-for="UserId">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="OrganizationId"></label>
|
<label asp-for="OrganizationId" class="form-label"></label>
|
||||||
<input type="text" class="form-control" asp-for="OrganizationId">
|
<input type="text" class="form-control" asp-for="OrganizationId">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="Date"></label>
|
<label asp-for="Date" class="form-label"></label>
|
||||||
<input type="datetime-local" class="form-control" asp-for="Date" required>
|
<input type="datetime-local" class="form-control" asp-for="Date" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="Type"></label>
|
<label asp-for="Type" class="form-label"></label>
|
||||||
<select class="form-control" asp-for="Type" required
|
<select class="form-select" asp-for="Type" required
|
||||||
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.TransactionType>()"></select>
|
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.TransactionType>()"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -41,24 +41,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="Amount"></label>
|
<label asp-for="Amount" class="form-label"></label>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend">
|
<span class="input-group-text">$</span>
|
||||||
<span class="input-group-text">$</span>
|
|
||||||
</div>
|
|
||||||
<input type="number" min="-1000000.00" max="1000000.00" step="0.01" class="form-control"
|
<input type="number" min="-1000000.00" max="1000000.00" step="0.01" class="form-control"
|
||||||
asp-for="Amount" required placeholder="ex. 10.00">
|
asp-for="Amount" required placeholder="ex. 10.00">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="RefundedAmount"></label>
|
<label asp-for="RefundedAmount" class="form-label"></label>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend">
|
<span class="input-group-text">$</span>
|
||||||
<span class="input-group-text">$</span>
|
|
||||||
</div>
|
|
||||||
<input type="number" min="0.01" max="1000000.00" step="0.01" class="form-control"
|
<input type="number" min="0.01" max="1000000.00" step="0.01" class="form-control"
|
||||||
asp-for="RefundedAmount" placeholder="ex. 10.00">
|
asp-for="RefundedAmount" placeholder="ex. 10.00">
|
||||||
</div>
|
</div>
|
||||||
@ -69,39 +65,35 @@
|
|||||||
<input type="checkbox" class="form-check-input" asp-for="Refunded">
|
<input type="checkbox" class="form-check-input" asp-for="Refunded">
|
||||||
<label class="form-check-label" asp-for="Refunded"></label>
|
<label class="form-check-label" asp-for="Refunded"></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="Details"></label>
|
<label asp-for="Details" class="form-label"></label>
|
||||||
<input type="text" class="form-control" asp-for="Details" required>
|
<input type="text" class="form-control" asp-for="Details" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<div class="form-group">
|
<label asp-for="Gateway" class="form-label"></label>
|
||||||
<label asp-for="Gateway"></label>
|
<select class="form-select" asp-for="Gateway"
|
||||||
<select class="form-control" asp-for="Gateway"
|
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||||
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
<option value="">--</option>
|
||||||
<option value="">--</option>
|
</select>
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="GatewayId"></label>
|
<label asp-for="GatewayId" class="form-label"></label>
|
||||||
<input type="text" class="form-control" asp-for="GatewayId">
|
<input type="text" class="form-control" asp-for="GatewayId">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<div class="form-group">
|
<label asp-for="PaymentMethod" class="form-label"></label>
|
||||||
<label asp-for="PaymentMethod"></label>
|
<select class="form-select" asp-for="PaymentMethod"
|
||||||
<select class="form-control" asp-for="PaymentMethod"
|
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.PaymentMethodType>()">
|
||||||
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.PaymentMethodType>()">
|
<option value="">--</option>
|
||||||
<option value="">--</option>
|
</select>
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,28 +9,28 @@
|
|||||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="UserId"></label>
|
<label asp-for="UserId" class="form-label"></label>
|
||||||
<input type="text" class="form-control" asp-for="UserId">
|
<input type="text" class="form-control" asp-for="UserId">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="OrganizationId"></label>
|
<label asp-for="OrganizationId" class="form-label"></label>
|
||||||
<input type="text" class="form-control" asp-for="OrganizationId">
|
<input type="text" class="form-control" asp-for="OrganizationId">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="InstallationId"></label>
|
<label asp-for="InstallationId" class="form-label"></label>
|
||||||
<input type="text" class="form-control" asp-for="InstallationId">
|
<input type="text" class="form-control" asp-for="InstallationId">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="Version"></label>
|
<label asp-for="Version" class="form-label"></label>
|
||||||
<input type="number" class="form-control" asp-for="Version">
|
<input type="number" class="form-control" asp-for="Version">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,17 +9,17 @@
|
|||||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="UserId"></label>
|
<label asp-for="UserId" class="form-label"></label>
|
||||||
<input type="text" class="form-control" asp-for="UserId">
|
<input type="text" class="form-control" asp-for="UserId">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="OrganizationId"></label>
|
<label asp-for="OrganizationId" class="form-label"></label>
|
||||||
<input type="text" class="form-control" asp-for="OrganizationId">
|
<input type="text" class="form-control" asp-for="OrganizationId">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</form>
|
@ -74,208 +74,204 @@
|
|||||||
<div class="alert alert-success"></div>
|
<div class="alert alert-success"></div>
|
||||||
}
|
}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||||
<div class="row">
|
<div class="row g-3">
|
||||||
<div class="col-6">
|
<div class="col-md-6">
|
||||||
<label asp-for="Filter.Status">Status</label>
|
<label class="form-label" asp-for="Filter.Status">Status</label>
|
||||||
<select asp-for="Filter.Status" name="filter.Status" class="form-control mr-2">
|
<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 == null" value="all">All</option>
|
||||||
<option asp-selected='Model.Filter.Status == "active"' value="active">Active</option>
|
<option asp-selected='Model.Filter.Status == "active"' value="active">Active</option>
|
||||||
<option asp-selected='Model.Filter.Status == "unpaid"' value="unpaid">Unpaid</option>
|
<option asp-selected='Model.Filter.Status == "unpaid"' value="unpaid">Unpaid</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-md-6">
|
||||||
<label asp-for="Filter.CurrentPeriodEnd">Current Period End</label>
|
<label class="form-label" asp-for="Filter.CurrentPeriodEnd">Current Period End</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-append">
|
|
||||||
<div class="input-group-text">
|
<div class="input-group-text">
|
||||||
<span class="mr-1">
|
<div class="form-check form-check-inline mb-0">
|
||||||
<input type="radio" class="mr-1" asp-for="Filter.CurrentPeriodEndRange" name="filter.CurrentPeriodEndRange" value="lt">Before
|
<input type="radio" class="form-check-input" asp-for="Filter.CurrentPeriodEndRange" value="lt" id="beforeRadio">
|
||||||
</span>
|
<label class="form-check-label me-2" for="beforeRadio">Before</label>
|
||||||
<input type="radio" asp-for="Filter.CurrentPeriodEndRange" name="filter.CurrentPeriodEndRange" value="gt">After
|
</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>
|
||||||
</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">
|
<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>
|
||||||
</div>
|
<hr/>
|
||||||
<div class="row mt-2">
|
<input type="checkbox" class="d-none" name="filter.SelectAll" id="selectAllInput" asp-for="@Model.Filter.SelectAll">
|
||||||
<div class="col-6">
|
<div class="text-center row d-flex justify-content-center">
|
||||||
<label asp-for="Filter.Price">Price ID</label>
|
<div id="selectAll" class="d-none col-8">
|
||||||
<select asp-for="Filter.Price" name="filter.Price" class="form-control mr-2">
|
All @Model.Items.Count subscriptions on this page are selected.<br/>
|
||||||
<option asp-selected="Model.Filter.Price == null" value="@null">All</option>
|
<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>
|
||||||
@foreach (var price in Model.Prices)
|
<span id="selectedAllConfirmation" class="d-none text-body-secondary">
|
||||||
{
|
<i class="fa fa-check"></i> All subscriptions for this search are selected.
|
||||||
<option asp-selected='@(Model.Filter.Price == price.Id)' value="@price.Id">@price.Id</option>
|
</span>
|
||||||
}
|
<div class="alert alert-warning mt-2" role="alert">
|
||||||
</select>
|
Please be aware that bulk operations may take several minutes to complete.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="table-responsive">
|
||||||
<label asp-for="Filter.TestClock">Test Clock</label>
|
<table class="table table-striped table-hover align-middle">
|
||||||
<select asp-for="Filter.TestClock" name="filter.TestClock" class="form-control mr-2">
|
<thead>
|
||||||
<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())
|
|
||||||
{
|
|
||||||
<tr>
|
<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>
|
</tr>
|
||||||
}
|
</thead>
|
||||||
else
|
<tbody>
|
||||||
{
|
@if (!Model.Items.Any())
|
||||||
@for (var i = 0; i < Model.Items.Count; i++)
|
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td colspan="6">No results to list.</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>
|
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
</tbody>
|
{
|
||||||
</table>
|
@for (var i = 0; i < Model.Items.Count; i++)
|
||||||
</div>
|
{
|
||||||
<nav class="d-inline-flex">
|
<tr>
|
||||||
<ul class="pagination">
|
<td>
|
||||||
@if (!string.IsNullOrWhiteSpace(Model.Filter.EndingBefore))
|
|
||||||
{
|
@{
|
||||||
<input type="hidden" asp-for="@Model.Filter.EndingBefore" value="@Model.Filter.EndingBefore">
|
var i0 = i;
|
||||||
<li class="page-item">
|
}
|
||||||
<button
|
<input type="hidden" asp-for="@Model.Items[i0].Subscription.Id" value="@Model.Items[i].Subscription.Id">
|
||||||
type="submit"
|
<input type="hidden" asp-for="@Model.Items[i0].Subscription.Status" value="@Model.Items[i].Subscription.Status">
|
||||||
class="page-link"
|
<input type="hidden" asp-for="@Model.Items[i0].Subscription.CurrentPeriodEnd" value="@Model.Items[i].Subscription.CurrentPeriodEnd">
|
||||||
name="action"
|
<input type="hidden" asp-for="@Model.Items[i0].Subscription.Customer.Email" value="@Model.Items[i].Subscription.Customer.Email">
|
||||||
asp-for="Action"
|
<input type="hidden" asp-for="@Model.Items[i0].Subscription.LatestInvoice.Status" value="@Model.Items[i].Subscription.LatestInvoice.Status">
|
||||||
value="@StripeSubscriptionsAction.PreviousPage">
|
<input type="hidden" asp-for="@Model.Items[i0].Subscription.LatestInvoice.Id" value="@Model.Items[i].Subscription.LatestInvoice.Id">
|
||||||
Previous
|
|
||||||
</button>
|
@for (var j = 0; j < Model.Items[i].Subscription.Items.Data.Count; j++)
|
||||||
</li>
|
{
|
||||||
}
|
var i1 = i;
|
||||||
else
|
var j1 = j;
|
||||||
{
|
<input
|
||||||
<li class="page-item disabled">
|
type="hidden"
|
||||||
<a class="page-link" href="#" tabindex="-1">Previous</a>
|
asp-for="@Model.Items[i1].Subscription.Items.Data[j1].Plan.Id"
|
||||||
</li>
|
value="@Model.Items[i].Subscription.Items.Data[j].Plan.Id">
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(Model.Filter.StartingAfter))
|
<div class="form-check">
|
||||||
{
|
|
||||||
<input type="hidden" asp-for="@Model.Filter.StartingAfter" value="@Model.Filter.StartingAfter">
|
@{
|
||||||
<li class="page-item">
|
var i2 = i;
|
||||||
<button class="page-link"
|
}
|
||||||
|
<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"
|
type="submit"
|
||||||
|
class="page-link"
|
||||||
name="action"
|
name="action"
|
||||||
asp-for="Action"
|
asp-for="Action"
|
||||||
value="@StripeSubscriptionsAction.NextPage">
|
value="@StripeSubscriptionsAction.PreviousPage">
|
||||||
Next
|
Previous
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" tabindex="-1">Previous</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.Filter.StartingAfter))
|
||||||
|
{
|
||||||
|
<input type="hidden" asp-for="@Model.Filter.StartingAfter" value="@Model.Filter.StartingAfter">
|
||||||
|
<li class="page-item">
|
||||||
|
<button class="page-link"
|
||||||
|
type="submit"
|
||||||
|
name="action"
|
||||||
|
asp-for="Action"
|
||||||
|
value="@StripeSubscriptionsAction.NextPage">
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" tabindex="-1">Next</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<span id="bulkActions" class="d-none ms-3">
|
||||||
|
<span class="d-inline-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary" name="action" asp-for="Action" value="@StripeSubscriptionsAction.Export">
|
||||||
|
Export
|
||||||
</button>
|
</button>
|
||||||
</li>
|
<button type="submit" class="btn btn-danger" name="action" asp-for="Action" value="@StripeSubscriptionsAction.BulkCancel">
|
||||||
}
|
Bulk Cancel
|
||||||
else
|
</button>
|
||||||
{
|
</span>
|
||||||
<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>
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</nav>
|
||||||
</nav>
|
|
||||||
</form>
|
</form>
|
||||||
|
@ -27,20 +27,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="post" enctype="multipart/form-data" asp-action="TaxRateUpload">
|
<form method="post" enctype="multipart/form-data" asp-action="TaxRateUpload">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<input type="file" name="file" />
|
<input type="file" class="form-control" name="file" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<input type="submit" value="Upload" class="btn btn-primary mb-2" />
|
<input type="submit" value="Upload" class="btn btn-primary" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<hr/>
|
<hr class="my-4">
|
||||||
<h2>View & Manage Tax Rates</h2>
|
<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">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-hover">
|
<table class="table table-striped table-hover align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 190px;">Id</th>
|
<th style="width: 190px;">Id</th>
|
||||||
@ -97,7 +97,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav>
|
<nav aria-label="Tax rates pagination">
|
||||||
<ul class="pagination">
|
<ul class="pagination">
|
||||||
@if(Model.PreviousPage.HasValue)
|
@if(Model.PreviousPage.HasValue)
|
||||||
{
|
{
|
||||||
|
@ -115,8 +115,8 @@
|
|||||||
<h2>Premium</h2>
|
<h2>Premium</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="MaxStorageGb"></label>
|
<label asp-for="MaxStorageGb" class="form-label"></label>
|
||||||
<input type="number" class="form-control" asp-for="MaxStorageGb" min="1" readonly='@(!canEditPremium)'>
|
<input type="number" class="form-control" asp-for="MaxStorageGb" min="1" readonly='@(!canEditPremium)'>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -131,14 +131,14 @@
|
|||||||
<h2>Licensing</h2>
|
<h2>Licensing</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="LicenseKey"></label>
|
<label asp-for="LicenseKey" class="form-label"></label>
|
||||||
<input type="text" class="form-control" asp-for="LicenseKey" readonly='@(!canEditLicensing)'>
|
<input type="text" class="form-control" asp-for="LicenseKey" readonly='@(!canEditLicensing)'>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="PremiumExpirationDate"></label>
|
<label asp-for="PremiumExpirationDate" class="form-label"></label>
|
||||||
<input type="datetime-local" class="form-control" asp-for="PremiumExpirationDate" readonly='@(!canEditLicensing)'>
|
<input type="datetime-local" class="form-control" asp-for="PremiumExpirationDate" readonly='@(!canEditLicensing)'>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -149,44 +149,38 @@
|
|||||||
<h2>Billing</h2>
|
<h2>Billing</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<div class="form-group">
|
<label asp-for="Gateway" class="form-label"></label>
|
||||||
<label asp-for="Gateway"></label>
|
<select class="form-select" asp-for="Gateway" disabled='@(canEditBilling ? null : "disabled")'
|
||||||
<select class="form-control" asp-for="Gateway" disabled='@(canEditBilling ? null : "disabled")'
|
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||||
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
<option value="">--</option>
|
||||||
<option value="">--</option>
|
</select>
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="GatewayCustomerId"></label>
|
<label asp-for="GatewayCustomerId" class="form-label"></label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" asp-for="GatewayCustomerId" readonly='@(!canEditBilling)'>
|
<input type="text" class="form-control" asp-for="GatewayCustomerId" readonly='@(!canEditBilling)'>
|
||||||
@if (canLaunchGateway)
|
@if (canLaunchGateway)
|
||||||
{
|
{
|
||||||
<div class="input-group-append">
|
<button class="btn btn-secondary" type="button" id="gateway-customer-link">
|
||||||
<button class="btn btn-secondary" type="button" id="gateway-customer-link">
|
<i class="fa fa-external-link"></i>
|
||||||
<i class="fa fa-external-link"></i>
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label asp-for="GatewaySubscriptionId"></label>
|
<label asp-for="GatewaySubscriptionId" class="form-label"></label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" asp-for="GatewaySubscriptionId" readonly='@(!canEditBilling)'>
|
<input type="text" class="form-control" asp-for="GatewaySubscriptionId" readonly='@(!canEditBilling)'>
|
||||||
@if (canLaunchGateway)
|
@if (canLaunchGateway)
|
||||||
{
|
{
|
||||||
<div class="input-group-append">
|
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
|
||||||
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
|
<i class="fa fa-external-link"></i>
|
||||||
<i class="fa fa-external-link"></i>
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -196,10 +190,10 @@
|
|||||||
</form>
|
</form>
|
||||||
<div class="d-flex mt-4">
|
<div class="d-flex mt-4">
|
||||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
<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)
|
@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
|
Upgrade Premium
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,16 @@
|
|||||||
|
|
||||||
<h1>Users</h1>
|
<h1>Users</h1>
|
||||||
|
|
||||||
<form class="form-inline mb-2" method="get">
|
<form class="row row-cols-lg-auto g-3 align-items-center mb-2" method="get">
|
||||||
<label class="sr-only" asp-for="Email">Email</label>
|
<div class="col-12">
|
||||||
<input type="text" class="form-control mb-2 mr-2" placeholder="Email" asp-for="Email" name="email">
|
<label class="visually-hidden" asp-for="Email">Email</label>
|
||||||
<button type="submit" class="btn btn-primary mb-2" title="Search"><i class="fa fa-search"></i> Search</button>
|
<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>
|
</form>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@ -49,7 +55,7 @@
|
|||||||
}
|
}
|
||||||
else
|
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)
|
@if (user.MaxStorageGb.HasValue && user.MaxStorageGb > 1)
|
||||||
{
|
{
|
||||||
@ -59,7 +65,7 @@
|
|||||||
}
|
}
|
||||||
else
|
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">
|
title="No Additional Storage">
|
||||||
</i>
|
</i>
|
||||||
}
|
}
|
||||||
@ -69,7 +75,7 @@
|
|||||||
}
|
}
|
||||||
else
|
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)
|
@if (user.TwoFactorEnabled)
|
||||||
{
|
{
|
||||||
@ -77,7 +83,7 @@
|
|||||||
}
|
}
|
||||||
else
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
35
src/Admin/package-lock.json
generated
35
src/Admin/package-lock.json
generated
@ -9,10 +9,9 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "4.6.2",
|
"bootstrap": "5.3.3",
|
||||||
"font-awesome": "4.7.0",
|
"font-awesome": "4.7.0",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"popper.js": "1.16.1",
|
|
||||||
"toastr": "2.1.4"
|
"toastr": "2.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -385,6 +384,17 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"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": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||||
@ -703,9 +713,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bootstrap": {
|
"node_modules/bootstrap": {
|
||||||
"version": "4.6.2",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
|
||||||
"integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==",
|
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@ -716,10 +726,8 @@
|
|||||||
"url": "https://opencollective.com/bootstrap"
|
"url": "https://opencollective.com/bootstrap"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"jquery": "1.9.1 - 3",
|
"@popperjs/core": "^2.11.8"
|
||||||
"popper.js": "^1.16.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/braces": {
|
"node_modules/braces": {
|
||||||
@ -1578,17 +1586,6 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.47",
|
"version": "8.4.47",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||||
|
@ -8,10 +8,9 @@
|
|||||||
"build": "webpack"
|
"build": "webpack"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "4.6.2",
|
"bootstrap": "5.3.3",
|
||||||
"font-awesome": "4.7.0",
|
"font-awesome": "4.7.0",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"popper.js": "1.16.1",
|
|
||||||
"toastr": "2.1.4"
|
"toastr": "2.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -13,8 +13,6 @@ module.exports = {
|
|||||||
entry: {
|
entry: {
|
||||||
site: [
|
site: [
|
||||||
path.resolve(__dirname, paths.sassDir, "site.scss"),
|
path.resolve(__dirname, paths.sassDir, "site.scss"),
|
||||||
|
|
||||||
"popper.js",
|
|
||||||
"bootstrap",
|
"bootstrap",
|
||||||
"jquery",
|
"jquery",
|
||||||
"font-awesome/css/font-awesome.css",
|
"font-awesome/css/font-awesome.css",
|
||||||
|
@ -2,15 +2,16 @@
|
|||||||
using Bit.Api.AdminConsole.Models.Response;
|
using Bit.Api.AdminConsole.Models.Response;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
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.Groups.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -89,11 +90,34 @@ public class GroupsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
public async Task<ListResponseModel<GroupDetailsResponseModel>> Get(Guid orgId)
|
public async Task<ListResponseModel<GroupDetailsResponseModel>> GetOrganizationGroups(Guid orgId)
|
||||||
{
|
{
|
||||||
var authorized =
|
var authResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAll);
|
||||||
(await _authorizationService.AuthorizeAsync(User, GroupOperations.ReadAll(orgId))).Succeeded;
|
if (!authResult.Succeeded)
|
||||||
if (!authorized)
|
{
|
||||||
|
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();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Enums;
|
|||||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
|
@ -289,9 +289,7 @@ public class OrganizationsController : Controller
|
|||||||
throw new BadRequestException(string.Empty, "User verification failed.");
|
throw new BadRequestException(string.Empty, "User verification failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
if (organization.IsValidClient())
|
||||||
|
|
||||||
if (consolidatedBillingEnabled && organization.IsValidClient())
|
|
||||||
{
|
{
|
||||||
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||||
|
|
||||||
@ -322,8 +320,7 @@ public class OrganizationsController : Controller
|
|||||||
throw new BadRequestException("Invalid token.");
|
throw new BadRequestException("Invalid token.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
if (organization.IsValidClient())
|
||||||
if (consolidatedBillingEnabled && organization.IsValidClient())
|
|
||||||
{
|
{
|
||||||
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||||
if (provider.IsBillable())
|
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
|
// 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);
|
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();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<UserSecretsId>bitwarden-Api</UserSecretsId>
|
<UserSecretsId>bitwarden-Api</UserSecretsId>
|
||||||
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
||||||
|
@ -4,15 +4,14 @@ using Bit.Api.Auth.Models.Response.TwoFactor;
|
|||||||
using Bit.Api.Models.Request;
|
using Bit.Api.Models.Request;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
|
using Bit.Core.Auth.Identity.TokenProviders;
|
||||||
using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces;
|
using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Auth.Utilities;
|
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Fido2NetLib;
|
using Fido2NetLib;
|
||||||
@ -29,11 +28,10 @@ public class TwoFactorController : Controller
|
|||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
private readonly GlobalSettings _globalSettings;
|
|
||||||
private readonly UserManager<User> _userManager;
|
private readonly UserManager<User> _userManager;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly IVerifyAuthRequestCommand _verifyAuthRequestCommand;
|
private readonly IVerifyAuthRequestCommand _verifyAuthRequestCommand;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IDuoUniversalTokenService _duoUniversalTokenService;
|
||||||
private readonly IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> _twoFactorAuthenticatorDataProtector;
|
private readonly IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> _twoFactorAuthenticatorDataProtector;
|
||||||
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmailTwoFactorSessionDataProtector;
|
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmailTwoFactorSessionDataProtector;
|
||||||
|
|
||||||
@ -41,22 +39,20 @@ public class TwoFactorController : Controller
|
|||||||
IUserService userService,
|
IUserService userService,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationService organizationService,
|
IOrganizationService organizationService,
|
||||||
GlobalSettings globalSettings,
|
|
||||||
UserManager<User> userManager,
|
UserManager<User> userManager,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IVerifyAuthRequestCommand verifyAuthRequestCommand,
|
IVerifyAuthRequestCommand verifyAuthRequestCommand,
|
||||||
IFeatureService featureService,
|
IDuoUniversalTokenService duoUniversalConfigService,
|
||||||
IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> twoFactorAuthenticatorDataProtector,
|
IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> twoFactorAuthenticatorDataProtector,
|
||||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmailTwoFactorSessionDataProtector)
|
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmailTwoFactorSessionDataProtector)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
_globalSettings = globalSettings;
|
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_verifyAuthRequestCommand = verifyAuthRequestCommand;
|
_verifyAuthRequestCommand = verifyAuthRequestCommand;
|
||||||
_featureService = featureService;
|
_duoUniversalTokenService = duoUniversalConfigService;
|
||||||
_twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector;
|
_twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector;
|
||||||
_ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector;
|
_ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector;
|
||||||
}
|
}
|
||||||
@ -184,21 +180,7 @@ public class TwoFactorController : Controller
|
|||||||
public async Task<TwoFactorDuoResponseModel> PutDuo([FromBody] UpdateTwoFactorDuoRequestModel model)
|
public async Task<TwoFactorDuoResponseModel> PutDuo([FromBody] UpdateTwoFactorDuoRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await CheckAsync(model, true);
|
var user = await CheckAsync(model, true);
|
||||||
try
|
if (!await _duoUniversalTokenService.ValidateDuoConfiguration(model.ClientSecret, model.ClientId, model.Host))
|
||||||
{
|
|
||||||
// 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)
|
|
||||||
{
|
{
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"Duo configuration settings are not valid. Please re-check the Duo Admin panel.");
|
"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();
|
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid) ?? throw new NotFoundException();
|
||||||
try
|
if (!await _duoUniversalTokenService.ValidateDuoConfiguration(model.ClientId, model.ClientSecret, model.Host))
|
||||||
{
|
|
||||||
// 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)
|
|
||||||
{
|
{
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"Duo configuration settings are not valid. Please re-check the Duo Admin panel.");
|
"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.Api.Auth.Models.Request.Accounts;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
|
using Bit.Core.Auth.Identity.TokenProviders;
|
||||||
using Bit.Core.Auth.Models;
|
using Bit.Core.Auth.Models;
|
||||||
using Bit.Core.Auth.Utilities;
|
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Fido2NetLib;
|
using Fido2NetLib;
|
||||||
|
|
||||||
@ -43,21 +43,16 @@ public class UpdateTwoFactorAuthenticatorRequestModel : SecretVerificationReques
|
|||||||
public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IValidatableObject
|
public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IValidatableObject
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
To support both v2 and v4 we need to remove the required annotation from the properties.
|
String lengths based on Duo's documentation
|
||||||
todo - the required annotation will be added back in PM-8107.
|
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]
|
[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 string Host { get; set; }
|
||||||
|
|
||||||
public User ToUser(User existingUser)
|
public User ToUser(User existingUser)
|
||||||
@ -65,22 +60,17 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
|
|||||||
var providers = existingUser.GetTwoFactorProviders();
|
var providers = existingUser.GetTwoFactorProviders();
|
||||||
if (providers == null)
|
if (providers == null)
|
||||||
{
|
{
|
||||||
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
providers = [];
|
||||||
}
|
}
|
||||||
else if (providers.ContainsKey(TwoFactorProviderType.Duo))
|
else if (providers.ContainsKey(TwoFactorProviderType.Duo))
|
||||||
{
|
{
|
||||||
providers.Remove(TwoFactorProviderType.Duo);
|
providers.Remove(TwoFactorProviderType.Duo);
|
||||||
}
|
}
|
||||||
|
|
||||||
Temporary_SyncDuoParams();
|
|
||||||
|
|
||||||
providers.Add(TwoFactorProviderType.Duo, new TwoFactorProvider
|
providers.Add(TwoFactorProviderType.Duo, new TwoFactorProvider
|
||||||
{
|
{
|
||||||
MetaData = new Dictionary<string, object>
|
MetaData = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
//todo - will remove SKey and IKey with PM-8107
|
|
||||||
["SKey"] = SecretKey,
|
|
||||||
["IKey"] = IntegrationKey,
|
|
||||||
["ClientSecret"] = ClientSecret,
|
["ClientSecret"] = ClientSecret,
|
||||||
["ClientId"] = ClientId,
|
["ClientId"] = ClientId,
|
||||||
["Host"] = Host
|
["Host"] = Host
|
||||||
@ -96,22 +86,17 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
|
|||||||
var providers = existingOrg.GetTwoFactorProviders();
|
var providers = existingOrg.GetTwoFactorProviders();
|
||||||
if (providers == null)
|
if (providers == null)
|
||||||
{
|
{
|
||||||
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
providers = [];
|
||||||
}
|
}
|
||||||
else if (providers.ContainsKey(TwoFactorProviderType.OrganizationDuo))
|
else if (providers.ContainsKey(TwoFactorProviderType.OrganizationDuo))
|
||||||
{
|
{
|
||||||
providers.Remove(TwoFactorProviderType.OrganizationDuo);
|
providers.Remove(TwoFactorProviderType.OrganizationDuo);
|
||||||
}
|
}
|
||||||
|
|
||||||
Temporary_SyncDuoParams();
|
|
||||||
|
|
||||||
providers.Add(TwoFactorProviderType.OrganizationDuo, new TwoFactorProvider
|
providers.Add(TwoFactorProviderType.OrganizationDuo, new TwoFactorProvider
|
||||||
{
|
{
|
||||||
MetaData = new Dictionary<string, object>
|
MetaData = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
//todo - will remove SKey and IKey with PM-8107
|
|
||||||
["SKey"] = SecretKey,
|
|
||||||
["IKey"] = IntegrationKey,
|
|
||||||
["ClientSecret"] = ClientSecret,
|
["ClientSecret"] = ClientSecret,
|
||||||
["ClientId"] = ClientId,
|
["ClientId"] = ClientId,
|
||||||
["Host"] = Host
|
["Host"] = Host
|
||||||
@ -124,34 +109,22 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
|
|||||||
|
|
||||||
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
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)]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
if (string.IsNullOrWhiteSpace(ClientSecret))
|
||||||
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;
|
results.Add(new ValidationResult("ClientSecret is required.", [nameof(ClientSecret)]));
|
||||||
IntegrationKey = ClientId;
|
|
||||||
}
|
}
|
||||||
else if (!string.IsNullOrWhiteSpace(SecretKey) && !string.IsNullOrWhiteSpace(IntegrationKey))
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Host) || !DuoUniversalTokenService.ValidDuoHost(Host))
|
||||||
{
|
{
|
||||||
ClientSecret = SecretKey;
|
results.Add(new ValidationResult("Host is invalid.", [nameof(Host)]));
|
||||||
ClientId = IntegrationKey;
|
|
||||||
}
|
}
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,37 +13,26 @@ public class TwoFactorDuoResponseModel : ResponseModel
|
|||||||
public TwoFactorDuoResponseModel(User user)
|
public TwoFactorDuoResponseModel(User user)
|
||||||
: base(ResponseObj)
|
: base(ResponseObj)
|
||||||
{
|
{
|
||||||
if (user == null)
|
ArgumentNullException.ThrowIfNull(user);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
|
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
|
||||||
Build(provider);
|
Build(provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TwoFactorDuoResponseModel(Organization org)
|
public TwoFactorDuoResponseModel(Organization organization)
|
||||||
: base(ResponseObj)
|
: base(ResponseObj)
|
||||||
{
|
{
|
||||||
if (org == null)
|
ArgumentNullException.ThrowIfNull(organization);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(org));
|
|
||||||
}
|
|
||||||
|
|
||||||
var provider = org.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
|
var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
|
||||||
Build(provider);
|
Build(provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Enabled { get; set; }
|
public bool Enabled { get; set; }
|
||||||
public string Host { 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 ClientSecret { get; set; }
|
||||||
public string ClientId { 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)
|
private void Build(TwoFactorProvider provider)
|
||||||
{
|
{
|
||||||
if (provider?.MetaData != null && provider.MetaData.Count > 0)
|
if (provider?.MetaData != null && provider.MetaData.Count > 0)
|
||||||
@ -54,36 +43,13 @@ public class TwoFactorDuoResponseModel : ResponseModel
|
|||||||
{
|
{
|
||||||
Host = (string)host;
|
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 (provider.MetaData.TryGetValue("ClientSecret", out var clientSecret))
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace((string)clientSecret))
|
ClientSecret = MaskSecret((string)clientSecret);
|
||||||
{
|
|
||||||
ClientSecret = MaskKey((string)clientSecret);
|
|
||||||
SecretKey = MaskKey((string)clientSecret);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (provider.MetaData.TryGetValue("ClientId", out var clientId))
|
if (provider.MetaData.TryGetValue("ClientId", out var clientId))
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace((string)clientId))
|
ClientId = (string)clientId;
|
||||||
{
|
|
||||||
ClientId = (string)clientId;
|
|
||||||
IntegrationKey = (string)clientId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -92,30 +58,7 @@ public class TwoFactorDuoResponseModel : ResponseModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
private static string MaskSecret(string key)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(key) || key.Length <= 6)
|
if (string.IsNullOrWhiteSpace(key) || key.Length <= 6)
|
||||||
{
|
{
|
||||||
|
@ -22,9 +22,9 @@ public abstract class BaseBillingController : Controller
|
|||||||
new ErrorResponseModel(message),
|
new ErrorResponseModel(message),
|
||||||
statusCode: StatusCodes.Status500InternalServerError);
|
statusCode: StatusCodes.Status500InternalServerError);
|
||||||
|
|
||||||
public static JsonHttpResult<ErrorResponseModel> Unauthorized() =>
|
public static JsonHttpResult<ErrorResponseModel> Unauthorized(string message = "Unauthorized.") =>
|
||||||
TypedResults.Json(
|
TypedResults.Json(
|
||||||
new ErrorResponseModel("Unauthorized."),
|
new ErrorResponseModel(message),
|
||||||
statusCode: StatusCodes.Status401Unauthorized);
|
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.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -9,7 +8,6 @@ namespace Bit.Api.Billing.Controllers;
|
|||||||
|
|
||||||
public abstract class BaseProviderController(
|
public abstract class BaseProviderController(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IFeatureService featureService,
|
|
||||||
ILogger<BaseProviderController> logger,
|
ILogger<BaseProviderController> logger,
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
IUserService userService) : BaseBillingController
|
IUserService userService) : BaseBillingController
|
||||||
@ -26,15 +24,6 @@ public abstract class BaseProviderController(
|
|||||||
Guid providerId,
|
Guid providerId,
|
||||||
Func<Guid, bool> checkAuthorization)
|
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);
|
var provider = await providerRepository.GetByIdAsync(providerId);
|
||||||
|
|
||||||
if (provider == null)
|
if (provider == null)
|
||||||
|
@ -19,14 +19,13 @@ namespace Bit.Api.Billing.Controllers;
|
|||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
public class ProviderBillingController(
|
public class ProviderBillingController(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IFeatureService featureService,
|
|
||||||
ILogger<BaseProviderController> logger,
|
ILogger<BaseProviderController> logger,
|
||||||
IProviderBillingService providerBillingService,
|
IProviderBillingService providerBillingService,
|
||||||
IProviderPlanRepository providerPlanRepository,
|
IProviderPlanRepository providerPlanRepository,
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
ISubscriberService subscriberService,
|
ISubscriberService subscriberService,
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
IUserService userService) : BaseProviderController(currentContext, featureService, logger, providerRepository, userService)
|
IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService)
|
||||||
{
|
{
|
||||||
[HttpGet("invoices")]
|
[HttpGet("invoices")]
|
||||||
public async Task<IResult> GetInvoicesAsync([FromRoute] Guid providerId)
|
public async Task<IResult> GetInvoicesAsync([FromRoute] Guid providerId)
|
||||||
|
@ -14,14 +14,13 @@ namespace Bit.Api.Billing.Controllers;
|
|||||||
[Route("providers/{providerId:guid}/clients")]
|
[Route("providers/{providerId:guid}/clients")]
|
||||||
public class ProviderClientsController(
|
public class ProviderClientsController(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IFeatureService featureService,
|
|
||||||
ILogger<BaseProviderController> logger,
|
ILogger<BaseProviderController> logger,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IProviderBillingService providerBillingService,
|
IProviderBillingService providerBillingService,
|
||||||
IProviderOrganizationRepository providerOrganizationRepository,
|
IProviderOrganizationRepository providerOrganizationRepository,
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
IProviderService providerService,
|
IProviderService providerService,
|
||||||
IUserService userService) : BaseProviderController(currentContext, featureService, logger, providerRepository, userService)
|
IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService)
|
||||||
{
|
{
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IResult> CreateAsync(
|
public async Task<IResult> CreateAsync(
|
||||||
@ -102,15 +101,27 @@ public class ProviderClientsController(
|
|||||||
|
|
||||||
var clientOrganization = await organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);
|
var clientOrganization = await organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);
|
||||||
|
|
||||||
if (clientOrganization.Seats != requestBody.AssignedSeats)
|
if (clientOrganization is not { Status: OrganizationStatusType.Managed })
|
||||||
{
|
{
|
||||||
await providerBillingService.AssignSeatsToClientOrganization(
|
return Error.ServerError();
|
||||||
provider,
|
|
||||||
clientOrganization,
|
|
||||||
requestBody.AssignedSeats);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.Name = requestBody.Name;
|
||||||
|
clientOrganization.Seats = requestBody.AssignedSeats;
|
||||||
|
|
||||||
await organizationRepository.ReplaceAsync(clientOrganization);
|
await organizationRepository.ReplaceAsync(clientOrganization);
|
||||||
|
|
||||||
|
@ -6,12 +6,14 @@ public record OrganizationMetadataResponse(
|
|||||||
bool IsEligibleForSelfHost,
|
bool IsEligibleForSelfHost,
|
||||||
bool IsManaged,
|
bool IsManaged,
|
||||||
bool IsOnSecretsManagerStandalone,
|
bool IsOnSecretsManagerStandalone,
|
||||||
bool IsSubscriptionUnpaid)
|
bool IsSubscriptionUnpaid,
|
||||||
|
bool HasSubscription)
|
||||||
{
|
{
|
||||||
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
|
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
|
||||||
=> new(
|
=> new(
|
||||||
metadata.IsEligibleForSelfHost,
|
metadata.IsEligibleForSelfHost,
|
||||||
metadata.IsManaged,
|
metadata.IsManaged,
|
||||||
metadata.IsOnSecretsManagerStandalone,
|
metadata.IsOnSecretsManagerStandalone,
|
||||||
metadata.IsSubscriptionUnpaid);
|
metadata.IsSubscriptionUnpaid,
|
||||||
|
metadata.HasSubscription);
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@ using Microsoft.OpenApi.Models;
|
|||||||
using Bit.SharedWeb.Utilities;
|
using Bit.SharedWeb.Utilities;
|
||||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Bit.Core.Auth.Identity;
|
|
||||||
using Bit.Core.Auth.UserFeatures;
|
using Bit.Core.Auth.UserFeatures;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
@ -32,10 +31,10 @@ using Bit.Core.Tools.Entities;
|
|||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
|
using Bit.Core.Auth.Identity.TokenProviders;
|
||||||
using Bit.Core.Tools.ReportFeatures;
|
using Bit.Core.Tools.ReportFeatures;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#if !OSS
|
#if !OSS
|
||||||
using Bit.Commercial.Core.SecretsManager;
|
using Bit.Commercial.Core.SecretsManager;
|
||||||
using Bit.Commercial.Core.Utilities;
|
using Bit.Commercial.Core.Utilities;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
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.IdentityServer;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,6 @@
|
|||||||
using Bit.Billing.Constants;
|
using Bit.Billing.Constants;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Entities;
|
using Bit.Core.Billing.Entities;
|
||||||
using Bit.Core.Billing.Enums;
|
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
@ -42,78 +41,52 @@ public class ProviderEventService(
|
|||||||
case HandledStripeWebhook.InvoiceCreated:
|
case HandledStripeWebhook.InvoiceCreated:
|
||||||
{
|
{
|
||||||
var clients =
|
var clients =
|
||||||
(await providerOrganizationRepository.GetManyDetailsByProviderAsync(parsedProviderId))
|
await providerOrganizationRepository.GetManyDetailsByProviderAsync(parsedProviderId);
|
||||||
.Where(providerOrganization => providerOrganization.Status == OrganizationStatusType.Managed);
|
|
||||||
|
|
||||||
var providerPlans = await providerPlanRepository.GetByProviderId(parsedProviderId);
|
var providerPlans = await providerPlanRepository.GetByProviderId(parsedProviderId);
|
||||||
|
|
||||||
var enterpriseProviderPlan =
|
var invoiceItems = new List<ProviderInvoiceItem>();
|
||||||
providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly);
|
|
||||||
|
|
||||||
var teamsProviderPlan =
|
foreach (var client in clients)
|
||||||
providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly);
|
|
||||||
|
|
||||||
if (enterpriseProviderPlan == null || !enterpriseProviderPlan.IsConfigured() ||
|
|
||||||
teamsProviderPlan == null || !teamsProviderPlan.IsConfigured())
|
|
||||||
{
|
{
|
||||||
logger.LogError("Provider {ProviderID} is missing or has misconfigured provider plans", parsedProviderId);
|
if (client.Status != OrganizationStatusType.Managed)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
throw new Exception("Cannot record invoice line items for Provider with missing or misconfigured provider plans");
|
var plan = StaticStore.Plans.Single(x => x.Name == client.Plan && providerPlans.Any(y => y.PlanType == x.Type));
|
||||||
}
|
|
||||||
|
|
||||||
var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
|
var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100;
|
||||||
|
|
||||||
var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
var discountedSeatPrice = plan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage;
|
||||||
|
|
||||||
var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100;
|
|
||||||
|
|
||||||
var discountedEnterpriseSeatPrice = enterprisePlan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage;
|
|
||||||
|
|
||||||
var discountedTeamsSeatPrice = teamsPlan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage;
|
|
||||||
|
|
||||||
var invoiceItems = clients.Select(client => new ProviderInvoiceItem
|
|
||||||
{
|
|
||||||
ProviderId = parsedProviderId,
|
|
||||||
InvoiceId = invoice.Id,
|
|
||||||
InvoiceNumber = invoice.Number,
|
|
||||||
ClientId = client.OrganizationId,
|
|
||||||
ClientName = client.OrganizationName,
|
|
||||||
PlanName = client.Plan,
|
|
||||||
AssignedSeats = client.Seats ?? 0,
|
|
||||||
UsedSeats = client.OccupiedSeats ?? 0,
|
|
||||||
Total = client.Plan == enterprisePlan.Name
|
|
||||||
? (client.Seats ?? 0) * discountedEnterpriseSeatPrice
|
|
||||||
: (client.Seats ?? 0) * discountedTeamsSeatPrice
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
if (enterpriseProviderPlan.PurchasedSeats is null or 0)
|
|
||||||
{
|
|
||||||
var enterpriseClientSeats = invoiceItems
|
|
||||||
.Where(item => item.PlanName == enterprisePlan.Name)
|
|
||||||
.Sum(item => item.AssignedSeats);
|
|
||||||
|
|
||||||
var unassignedEnterpriseSeats = enterpriseProviderPlan.SeatMinimum - enterpriseClientSeats ?? 0;
|
|
||||||
|
|
||||||
invoiceItems.Add(new ProviderInvoiceItem
|
invoiceItems.Add(new ProviderInvoiceItem
|
||||||
{
|
{
|
||||||
ProviderId = parsedProviderId,
|
ProviderId = parsedProviderId,
|
||||||
InvoiceId = invoice.Id,
|
InvoiceId = invoice.Id,
|
||||||
InvoiceNumber = invoice.Number,
|
InvoiceNumber = invoice.Number,
|
||||||
ClientName = "Unassigned seats",
|
ClientId = client.OrganizationId,
|
||||||
PlanName = enterprisePlan.Name,
|
ClientName = client.OrganizationName,
|
||||||
AssignedSeats = unassignedEnterpriseSeats,
|
PlanName = client.Plan,
|
||||||
UsedSeats = 0,
|
AssignedSeats = client.Seats ?? 0,
|
||||||
Total = unassignedEnterpriseSeats * discountedEnterpriseSeatPrice
|
UsedSeats = client.OccupiedSeats ?? 0,
|
||||||
|
Total = (client.Seats ?? 0) * discountedSeatPrice
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (teamsProviderPlan.PurchasedSeats is null or 0)
|
foreach (var providerPlan in providerPlans.Where(x => x.PurchasedSeats is null or 0))
|
||||||
{
|
{
|
||||||
var teamsClientSeats = invoiceItems
|
var plan = StaticStore.GetPlan(providerPlan.PlanType);
|
||||||
.Where(item => item.PlanName == teamsPlan.Name)
|
|
||||||
|
var clientSeats = invoiceItems
|
||||||
|
.Where(item => item.PlanName == plan.Name)
|
||||||
.Sum(item => item.AssignedSeats);
|
.Sum(item => item.AssignedSeats);
|
||||||
|
|
||||||
var unassignedTeamsSeats = teamsProviderPlan.SeatMinimum - teamsClientSeats ?? 0;
|
var unassignedSeats = providerPlan.SeatMinimum - clientSeats ?? 0;
|
||||||
|
|
||||||
|
var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100;
|
||||||
|
|
||||||
|
var discountedSeatPrice = plan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage;
|
||||||
|
|
||||||
invoiceItems.Add(new ProviderInvoiceItem
|
invoiceItems.Add(new ProviderInvoiceItem
|
||||||
{
|
{
|
||||||
@ -121,10 +94,10 @@ public class ProviderEventService(
|
|||||||
InvoiceId = invoice.Id,
|
InvoiceId = invoice.Id,
|
||||||
InvoiceNumber = invoice.Number,
|
InvoiceNumber = invoice.Number,
|
||||||
ClientName = "Unassigned seats",
|
ClientName = "Unassigned seats",
|
||||||
PlanName = teamsPlan.Name,
|
PlanName = plan.Name,
|
||||||
AssignedSeats = unassignedTeamsSeats,
|
AssignedSeats = unassignedSeats,
|
||||||
UsedSeats = 0,
|
UsedSeats = 0,
|
||||||
Total = unassignedTeamsSeats * discountedTeamsSeatPrice
|
Total = unassignedSeats * discountedSeatPrice
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
<input asp-for="Email" type="email" class="form-control" placeholder="ex. john@example.com"
|
<input asp-for="Email" type="email" class="form-control" placeholder="ex. john@example.com"
|
||||||
required autofocus>
|
required autofocus>
|
||||||
<span asp-validation-for="Email" class="invalid-feedback"></span>
|
<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>
|
</div>
|
||||||
<button class="btn btn-primary btn-block" type="submit">Continue</button>
|
<button class="btn btn-primary btn-block" type="submit">Continue</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -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
|
#nullable enable
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.Context;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Context;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||||
@ -7,14 +7,10 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authoriza
|
|||||||
public class OrganizationUserUserMiniDetailsAuthorizationHandler :
|
public class OrganizationUserUserMiniDetailsAuthorizationHandler :
|
||||||
AuthorizationHandler<OrganizationUserUserMiniDetailsOperationRequirement, OrganizationScope>
|
AuthorizationHandler<OrganizationUserUserMiniDetailsOperationRequirement, OrganizationScope>
|
||||||
{
|
{
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
|
|
||||||
public OrganizationUserUserMiniDetailsAuthorizationHandler(
|
public OrganizationUserUserMiniDetailsAuthorizationHandler(ICurrentContext currentContext)
|
||||||
IApplicationCacheService applicationCacheService,
|
|
||||||
ICurrentContext currentContext)
|
|
||||||
{
|
{
|
||||||
_applicationCacheService = applicationCacheService;
|
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A typed wrapper for an organization Guid. This is used for authorization checks
|
/// A typed wrapper for an organization Guid. This is used for authorization checks
|
@ -15,6 +15,7 @@ using Bit.Core.Auth.Models.Business.Tokenables;
|
|||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Models.Sales;
|
using Bit.Core.Billing.Models.Sales;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -444,13 +445,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup)
|
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);
|
var plan = StaticStore.GetPlan(signup.Plan);
|
||||||
|
|
||||||
ValidatePlan(plan, signup.AdditionalSeats, "Password Manager");
|
ValidatePlan(plan, signup.AdditionalSeats, "Password Manager");
|
||||||
@ -1443,10 +1437,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
if (provider is { Enabled: true })
|
if (provider is { Enabled: true })
|
||||||
{
|
{
|
||||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
if (provider.IsBillable())
|
||||||
|
|
||||||
if (consolidatedBillingEnabled && provider.Type == ProviderType.Msp &&
|
|
||||||
provider.Status == ProviderStatusType.Billable)
|
|
||||||
{
|
{
|
||||||
return (false, "Seat limit has been reached. Please contact your provider to add more seats.");
|
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 Microsoft.Extensions.DependencyInjection;
|
||||||
using OtpNet;
|
using OtpNet;
|
||||||
|
|
||||||
namespace Bit.Core.Auth.Identity;
|
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||||
|
|
||||||
public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider<User>
|
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.Caching.Distributed;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Bit.Core.Auth.Identity;
|
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||||
|
|
||||||
public class EmailTokenProvider : IUserTwoFactorTokenProvider<User>
|
public class EmailTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||||
{
|
{
|
@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Identity;
|
|||||||
using Microsoft.Extensions.Caching.Distributed;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Bit.Core.Auth.Identity;
|
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||||
|
|
||||||
public class EmailTwoFactorTokenProvider : EmailTokenProvider
|
public class EmailTwoFactorTokenProvider : EmailTokenProvider
|
||||||
{
|
{
|
@ -1,7 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
|
||||||
namespace Bit.Core.Auth.Identity;
|
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||||
|
|
||||||
public interface IOrganizationTwoFactorTokenProvider
|
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.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Bit.Core.Auth.Identity;
|
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||||
|
|
||||||
public class TwoFactorRememberTokenProvider : DataProtectorTokenProvider<User>
|
public class TwoFactorRememberTokenProvider : DataProtectorTokenProvider<User>
|
||||||
{
|
{
|
@ -10,7 +10,7 @@ using Fido2NetLib.Objects;
|
|||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Bit.Core.Auth.Identity;
|
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||||
|
|
||||||
public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
|
public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||||
{
|
{
|
@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Identity;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using YubicoDotNetClient;
|
using YubicoDotNetClient;
|
||||||
|
|
||||||
namespace Bit.Core.Auth.Identity;
|
namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||||
|
|
||||||
public class YubicoOtpTokenProvider : IUserTwoFactorTokenProvider<User>
|
public class YubicoOtpTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||||
{
|
{
|
||||||
@ -24,7 +24,7 @@ public class YubicoOtpTokenProvider : IUserTwoFactorTokenProvider<User>
|
|||||||
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
||||||
{
|
{
|
||||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||||
if (!(await userService.CanAccessPremium(user)))
|
if (!await userService.CanAccessPremium(user))
|
||||||
{
|
{
|
||||||
return false;
|
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)
|
public async Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user)
|
||||||
{
|
{
|
||||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||||
if (!(await userService.CanAccessPremium(user)))
|
if (!await userService.CanAccessPremium(user))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
@ -1,277 +0,0 @@
|
|||||||
/*
|
|
||||||
Original source modified from https://github.com/duosecurity/duo_api_csharp
|
|
||||||
|
|
||||||
=============================================================================
|
|
||||||
=============================================================================
|
|
||||||
|
|
||||||
Copyright (c) 2018 Duo Security
|
|
||||||
All rights reserved
|
|
||||||
*/
|
|
||||||
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Net;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Web;
|
|
||||||
using Bit.Core.Models.Api.Response.Duo;
|
|
||||||
|
|
||||||
namespace Bit.Core.Auth.Utilities;
|
|
||||||
|
|
||||||
public class DuoApi
|
|
||||||
{
|
|
||||||
private const string UrlScheme = "https";
|
|
||||||
private const string UserAgent = "Bitwarden_DuoAPICSharp/1.0 (.NET Core)";
|
|
||||||
|
|
||||||
private readonly string _host;
|
|
||||||
private readonly string _ikey;
|
|
||||||
private readonly string _skey;
|
|
||||||
|
|
||||||
private readonly HttpClient _httpClient = new();
|
|
||||||
|
|
||||||
public DuoApi(string ikey, string skey, string host)
|
|
||||||
{
|
|
||||||
_ikey = ikey;
|
|
||||||
_skey = skey;
|
|
||||||
_host = host;
|
|
||||||
|
|
||||||
if (!ValidHost(host))
|
|
||||||
{
|
|
||||||
throw new DuoException("Invalid Duo host configured.", new ArgumentException(nameof(host)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool ValidHost(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 static string CanonicalizeParams(Dictionary<string, string> parameters)
|
|
||||||
{
|
|
||||||
var ret = new List<string>();
|
|
||||||
foreach (var pair in parameters)
|
|
||||||
{
|
|
||||||
var p = string.Format("{0}={1}", HttpUtility.UrlEncode(pair.Key), HttpUtility.UrlEncode(pair.Value));
|
|
||||||
// Signatures require upper-case hex digits.
|
|
||||||
p = Regex.Replace(p, "(%[0-9A-Fa-f][0-9A-Fa-f])", c => c.Value.ToUpperInvariant());
|
|
||||||
// Escape only the expected characters.
|
|
||||||
p = Regex.Replace(p, "([!'()*])", c => "%" + Convert.ToByte(c.Value[0]).ToString("X"));
|
|
||||||
p = p.Replace("%7E", "~");
|
|
||||||
// UrlEncode converts space (" ") to "+". The
|
|
||||||
// signature algorithm requires "%20" instead. Actual
|
|
||||||
// + has already been replaced with %2B.
|
|
||||||
p = p.Replace("+", "%20");
|
|
||||||
ret.Add(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.Sort(StringComparer.Ordinal);
|
|
||||||
return string.Join("&", ret.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected string CanonicalizeRequest(string method, string path, string canonParams, string date)
|
|
||||||
{
|
|
||||||
string[] lines = {
|
|
||||||
date,
|
|
||||||
method.ToUpperInvariant(),
|
|
||||||
_host.ToLower(),
|
|
||||||
path,
|
|
||||||
canonParams,
|
|
||||||
};
|
|
||||||
return string.Join("\n", lines);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Sign(string method, string path, string canonParams, string date)
|
|
||||||
{
|
|
||||||
var canon = CanonicalizeRequest(method, path, canonParams, date);
|
|
||||||
var sig = HmacSign(canon);
|
|
||||||
var auth = string.Concat(_ikey, ':', sig);
|
|
||||||
return string.Concat("Basic ", Encode64(auth));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <param name="timeout">The request timeout, in milliseconds.
|
|
||||||
/// Specify 0 to use the system-default timeout. Use caution if
|
|
||||||
/// you choose to specify a custom timeout - some API
|
|
||||||
/// calls (particularly in the Auth APIs) will not
|
|
||||||
/// return a response until an out-of-band authentication process
|
|
||||||
/// has completed. In some cases, this may take as much as a
|
|
||||||
/// small number of minutes.</param>
|
|
||||||
private async Task<(string result, HttpStatusCode statusCode)> ApiCall(string method, string path, Dictionary<string, string> parameters, int timeout)
|
|
||||||
{
|
|
||||||
if (parameters == null)
|
|
||||||
{
|
|
||||||
parameters = new Dictionary<string, string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var canonParams = CanonicalizeParams(parameters);
|
|
||||||
var query = string.Empty;
|
|
||||||
if (!method.Equals("POST") && !method.Equals("PUT"))
|
|
||||||
{
|
|
||||||
if (parameters.Count > 0)
|
|
||||||
{
|
|
||||||
query = "?" + canonParams;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var url = $"{UrlScheme}://{_host}{path}{query}";
|
|
||||||
|
|
||||||
var dateString = RFC822UtcNow();
|
|
||||||
var auth = Sign(method, path, canonParams, dateString);
|
|
||||||
|
|
||||||
var request = new HttpRequestMessage
|
|
||||||
{
|
|
||||||
Method = new HttpMethod(method),
|
|
||||||
RequestUri = new Uri(url),
|
|
||||||
};
|
|
||||||
request.Headers.Add("Authorization", auth);
|
|
||||||
request.Headers.Add("X-Duo-Date", dateString);
|
|
||||||
request.Headers.UserAgent.ParseAdd(UserAgent);
|
|
||||||
|
|
||||||
if (timeout > 0)
|
|
||||||
{
|
|
||||||
_httpClient.Timeout = TimeSpan.FromMilliseconds(timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (method.Equals("POST") || method.Equals("PUT"))
|
|
||||||
{
|
|
||||||
request.Content = new StringContent(canonParams, Encoding.UTF8, "application/x-www-form-urlencoded");
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = await _httpClient.SendAsync(request);
|
|
||||||
var result = await response.Content.ReadAsStringAsync();
|
|
||||||
var statusCode = response.StatusCode;
|
|
||||||
return (result, statusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Response> JSONApiCall(string method, string path, Dictionary<string, string> parameters = null)
|
|
||||||
{
|
|
||||||
return await JSONApiCall(method, path, parameters, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <param name="timeout">The request timeout, in milliseconds.
|
|
||||||
/// Specify 0 to use the system-default timeout. Use caution if
|
|
||||||
/// you choose to specify a custom timeout - some API
|
|
||||||
/// calls (particularly in the Auth APIs) will not
|
|
||||||
/// return a response until an out-of-band authentication process
|
|
||||||
/// has completed. In some cases, this may take as much as a
|
|
||||||
/// small number of minutes.</param>
|
|
||||||
private async Task<Response> JSONApiCall(string method, string path, Dictionary<string, string> parameters, int timeout)
|
|
||||||
{
|
|
||||||
var (res, statusCode) = await ApiCall(method, path, parameters, timeout);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var obj = JsonSerializer.Deserialize<DuoResponseModel>(res);
|
|
||||||
if (obj.Stat == "OK")
|
|
||||||
{
|
|
||||||
return obj.Response;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ApiException(obj.Code ?? 0, (int)statusCode, obj.Message, obj.MessageDetail);
|
|
||||||
}
|
|
||||||
catch (ApiException)
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
throw new BadResponseException((int)statusCode, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int? ToNullableInt(string s)
|
|
||||||
{
|
|
||||||
int i;
|
|
||||||
if (int.TryParse(s, out i))
|
|
||||||
{
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string HmacSign(string data)
|
|
||||||
{
|
|
||||||
var keyBytes = Encoding.ASCII.GetBytes(_skey);
|
|
||||||
var dataBytes = Encoding.ASCII.GetBytes(data);
|
|
||||||
|
|
||||||
using (var hmac = new HMACSHA1(keyBytes))
|
|
||||||
{
|
|
||||||
var hash = hmac.ComputeHash(dataBytes);
|
|
||||||
var hex = BitConverter.ToString(hash);
|
|
||||||
return hex.Replace("-", string.Empty).ToLower();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Encode64(string plaintext)
|
|
||||||
{
|
|
||||||
var plaintextBytes = Encoding.ASCII.GetBytes(plaintext);
|
|
||||||
return Convert.ToBase64String(plaintextBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string RFC822UtcNow()
|
|
||||||
{
|
|
||||||
// Can't use the "zzzz" format because it adds a ":"
|
|
||||||
// between the offset's hours and minutes.
|
|
||||||
var dateString = DateTime.UtcNow.ToString("ddd, dd MMM yyyy HH:mm:ss", CultureInfo.InvariantCulture);
|
|
||||||
var offset = 0;
|
|
||||||
var zone = "+" + offset.ToString(CultureInfo.InvariantCulture).PadLeft(2, '0');
|
|
||||||
dateString += " " + zone.PadRight(5, '0');
|
|
||||||
return dateString;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DuoException : Exception
|
|
||||||
{
|
|
||||||
public int HttpStatus { get; private set; }
|
|
||||||
|
|
||||||
public DuoException(string message, Exception inner)
|
|
||||||
: base(message, inner)
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public DuoException(int httpStatus, string message, Exception inner)
|
|
||||||
: base(message, inner)
|
|
||||||
{
|
|
||||||
HttpStatus = httpStatus;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ApiException : DuoException
|
|
||||||
{
|
|
||||||
public int Code { get; private set; }
|
|
||||||
public string ApiMessage { get; private set; }
|
|
||||||
public string ApiMessageDetail { get; private set; }
|
|
||||||
|
|
||||||
public ApiException(int code, int httpStatus, string apiMessage, string apiMessageDetail)
|
|
||||||
: base(httpStatus, FormatMessage(code, apiMessage, apiMessageDetail), null)
|
|
||||||
{
|
|
||||||
Code = code;
|
|
||||||
ApiMessage = apiMessage;
|
|
||||||
ApiMessageDetail = apiMessageDetail;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatMessage(int code, string apiMessage, string apiMessageDetail)
|
|
||||||
{
|
|
||||||
return string.Format("Duo API Error {0}: '{1}' ('{2}')", code, apiMessage, apiMessageDetail);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BadResponseException : DuoException
|
|
||||||
{
|
|
||||||
public BadResponseException(int httpStatus, Exception inner)
|
|
||||||
: base(httpStatus, FormatMessage(httpStatus, inner), inner)
|
|
||||||
{ }
|
|
||||||
|
|
||||||
private static string FormatMessage(int httpStatus, Exception inner)
|
|
||||||
{
|
|
||||||
var innerMessage = "(null)";
|
|
||||||
if (inner != null)
|
|
||||||
{
|
|
||||||
innerMessage = string.Format("'{0}'", inner.Message);
|
|
||||||
}
|
|
||||||
return string.Format("Got error {0} with HTTP Status {1}", innerMessage, httpStatus);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,240 +0,0 @@
|
|||||||
/*
|
|
||||||
Original source modified from https://github.com/duosecurity/duo_dotnet
|
|
||||||
|
|
||||||
=============================================================================
|
|
||||||
=============================================================================
|
|
||||||
|
|
||||||
ref: https://github.com/duosecurity/duo_dotnet/blob/master/LICENSE
|
|
||||||
|
|
||||||
Copyright (c) 2011, Duo Security, Inc.
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions
|
|
||||||
are met:
|
|
||||||
|
|
||||||
1. Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
2. Redistributions in binary form must reproduce the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer in the
|
|
||||||
documentation and/or other materials provided with the distribution.
|
|
||||||
3. The name of the author may not be used to endorse or promote products
|
|
||||||
derived from this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
|
||||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
|
||||||
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
||||||
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
|
||||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
||||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Bit.Core.Auth.Utilities.Duo;
|
|
||||||
|
|
||||||
public static class DuoWeb
|
|
||||||
{
|
|
||||||
private const string DuoProfix = "TX";
|
|
||||||
private const string AppPrefix = "APP";
|
|
||||||
private const string AuthPrefix = "AUTH";
|
|
||||||
private const int DuoExpire = 300;
|
|
||||||
private const int AppExpire = 3600;
|
|
||||||
private const int IKeyLength = 20;
|
|
||||||
private const int SKeyLength = 40;
|
|
||||||
private const int AKeyLength = 40;
|
|
||||||
|
|
||||||
public static string ErrorUser = "ERR|The username passed to sign_request() is invalid.";
|
|
||||||
public static string ErrorIKey = "ERR|The Duo integration key passed to sign_request() is invalid.";
|
|
||||||
public static string ErrorSKey = "ERR|The Duo secret key passed to sign_request() is invalid.";
|
|
||||||
public static string ErrorAKey = "ERR|The application secret key passed to sign_request() must be at least " +
|
|
||||||
"40 characters.";
|
|
||||||
public static string ErrorUnknown = "ERR|An unknown error has occurred.";
|
|
||||||
|
|
||||||
// throw on invalid bytes
|
|
||||||
private static Encoding _encoding = new UTF8Encoding(false, true);
|
|
||||||
private static DateTime _epoc = new DateTime(1970, 1, 1);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate a signed request for Duo authentication.
|
|
||||||
/// The returned value should be passed into the Duo.init() call
|
|
||||||
/// in the rendered web page used for Duo authentication.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ikey">Duo integration key</param>
|
|
||||||
/// <param name="skey">Duo secret key</param>
|
|
||||||
/// <param name="akey">Application secret key</param>
|
|
||||||
/// <param name="username">Primary-authenticated username</param>
|
|
||||||
/// <param name="currentTime">(optional) The current UTC time</param>
|
|
||||||
/// <returns>signed request</returns>
|
|
||||||
public static string SignRequest(string ikey, string skey, string akey, string username,
|
|
||||||
DateTime? currentTime = null)
|
|
||||||
{
|
|
||||||
string duoSig;
|
|
||||||
string appSig;
|
|
||||||
|
|
||||||
var currentTimeValue = currentTime ?? DateTime.UtcNow;
|
|
||||||
|
|
||||||
if (username == string.Empty)
|
|
||||||
{
|
|
||||||
return ErrorUser;
|
|
||||||
}
|
|
||||||
if (username.Contains("|"))
|
|
||||||
{
|
|
||||||
return ErrorUser;
|
|
||||||
}
|
|
||||||
if (ikey.Length != IKeyLength)
|
|
||||||
{
|
|
||||||
return ErrorIKey;
|
|
||||||
}
|
|
||||||
if (skey.Length != SKeyLength)
|
|
||||||
{
|
|
||||||
return ErrorSKey;
|
|
||||||
}
|
|
||||||
if (akey.Length < AKeyLength)
|
|
||||||
{
|
|
||||||
return ErrorAKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
duoSig = SignVals(skey, username, ikey, DuoProfix, DuoExpire, currentTimeValue);
|
|
||||||
appSig = SignVals(akey, username, ikey, AppPrefix, AppExpire, currentTimeValue);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return ErrorUnknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"{duoSig}:{appSig}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validate the signed response returned from Duo.
|
|
||||||
/// Returns the username of the authenticated user, or null.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ikey">Duo integration key</param>
|
|
||||||
/// <param name="skey">Duo secret key</param>
|
|
||||||
/// <param name="akey">Application secret key</param>
|
|
||||||
/// <param name="sigResponse">The signed response POST'ed to the server</param>
|
|
||||||
/// <param name="currentTime">(optional) The current UTC time</param>
|
|
||||||
/// <returns>authenticated username, or null</returns>
|
|
||||||
public static string VerifyResponse(string ikey, string skey, string akey, string sigResponse,
|
|
||||||
DateTime? currentTime = null)
|
|
||||||
{
|
|
||||||
string authUser = null;
|
|
||||||
string appUser = null;
|
|
||||||
var currentTimeValue = currentTime ?? DateTime.UtcNow;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var sigs = sigResponse.Split(':');
|
|
||||||
var authSig = sigs[0];
|
|
||||||
var appSig = sigs[1];
|
|
||||||
|
|
||||||
authUser = ParseVals(skey, authSig, AuthPrefix, ikey, currentTimeValue);
|
|
||||||
appUser = ParseVals(akey, appSig, AppPrefix, ikey, currentTimeValue);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authUser != appUser)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return authUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string SignVals(string key, string username, string ikey, string prefix, long expire,
|
|
||||||
DateTime currentTime)
|
|
||||||
{
|
|
||||||
var ts = (long)(currentTime - _epoc).TotalSeconds;
|
|
||||||
expire = ts + expire;
|
|
||||||
var val = $"{username}|{ikey}|{expire.ToString()}";
|
|
||||||
var cookie = $"{prefix}|{Encode64(val)}";
|
|
||||||
var sig = Sign(key, cookie);
|
|
||||||
return $"{cookie}|{sig}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ParseVals(string key, string val, string prefix, string ikey, DateTime currentTime)
|
|
||||||
{
|
|
||||||
var ts = (long)(currentTime - _epoc).TotalSeconds;
|
|
||||||
|
|
||||||
var parts = val.Split('|');
|
|
||||||
if (parts.Length != 3)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var uPrefix = parts[0];
|
|
||||||
var uB64 = parts[1];
|
|
||||||
var uSig = parts[2];
|
|
||||||
|
|
||||||
var sig = Sign(key, $"{uPrefix}|{uB64}");
|
|
||||||
if (Sign(key, sig) != Sign(key, uSig))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uPrefix != prefix)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var cookie = Decode64(uB64);
|
|
||||||
var cookieParts = cookie.Split('|');
|
|
||||||
if (cookieParts.Length != 3)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var username = cookieParts[0];
|
|
||||||
var uIKey = cookieParts[1];
|
|
||||||
var expire = cookieParts[2];
|
|
||||||
|
|
||||||
if (uIKey != ikey)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var expireTs = Convert.ToInt32(expire);
|
|
||||||
if (ts >= expireTs)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return username;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Sign(string skey, string data)
|
|
||||||
{
|
|
||||||
var keyBytes = Encoding.ASCII.GetBytes(skey);
|
|
||||||
var dataBytes = Encoding.ASCII.GetBytes(data);
|
|
||||||
|
|
||||||
using (var hmac = new HMACSHA1(keyBytes))
|
|
||||||
{
|
|
||||||
var hash = hmac.ComputeHash(dataBytes);
|
|
||||||
var hex = BitConverter.ToString(hash);
|
|
||||||
return hex.Replace("-", "").ToLower();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Encode64(string plaintext)
|
|
||||||
{
|
|
||||||
var plaintextBytes = _encoding.GetBytes(plaintext);
|
|
||||||
return Convert.ToBase64String(plaintextBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Decode64(string encoded)
|
|
||||||
{
|
|
||||||
var plaintextBytes = Convert.FromBase64String(encoded);
|
|
||||||
return _encoding.GetString(plaintextBytes);
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,22 +11,29 @@ namespace Bit.Core.Billing.Extensions;
|
|||||||
public static class BillingExtensions
|
public static class BillingExtensions
|
||||||
{
|
{
|
||||||
public static bool IsBillable(this Provider provider) =>
|
public static bool IsBillable(this Provider provider) =>
|
||||||
provider.SupportsConsolidatedBilling() && provider.Status == ProviderStatusType.Billable;
|
provider is
|
||||||
|
{
|
||||||
|
Type: ProviderType.Msp or ProviderType.MultiOrganizationEnterprise,
|
||||||
|
Status: ProviderStatusType.Billable
|
||||||
|
};
|
||||||
|
|
||||||
public static bool SupportsConsolidatedBilling(this Provider provider)
|
public static bool SupportsConsolidatedBilling(this ProviderType providerType)
|
||||||
=> provider.Type is ProviderType.Msp or ProviderType.MultiOrganizationEnterprise;
|
=> providerType is ProviderType.Msp or ProviderType.MultiOrganizationEnterprise;
|
||||||
|
|
||||||
public static bool IsValidClient(this Organization organization)
|
public static bool IsValidClient(this Organization organization)
|
||||||
=> organization is
|
=> organization is
|
||||||
{
|
{
|
||||||
Seats: not null,
|
Seats: not null,
|
||||||
Status: OrganizationStatusType.Managed,
|
Status: OrganizationStatusType.Managed,
|
||||||
PlanType: PlanType.TeamsMonthly or PlanType.EnterpriseMonthly
|
PlanType: PlanType.TeamsMonthly or PlanType.EnterpriseMonthly or PlanType.EnterpriseAnnually
|
||||||
};
|
};
|
||||||
|
|
||||||
public static bool IsStripeEnabled(this ISubscriber subscriber)
|
public static bool IsStripeEnabled(this ISubscriber subscriber)
|
||||||
=> !string.IsNullOrEmpty(subscriber.GatewayCustomerId) &&
|
=> subscriber is
|
||||||
!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId);
|
{
|
||||||
|
GatewayCustomerId: not null and not "",
|
||||||
|
GatewaySubscriptionId: not null and not ""
|
||||||
|
};
|
||||||
|
|
||||||
public static bool IsUnverifiedBankAccount(this SetupIntent setupIntent) =>
|
public static bool IsUnverifiedBankAccount(this SetupIntent setupIntent) =>
|
||||||
setupIntent is
|
setupIntent is
|
||||||
|
@ -4,4 +4,5 @@ public record OrganizationMetadata(
|
|||||||
bool IsEligibleForSelfHost,
|
bool IsEligibleForSelfHost,
|
||||||
bool IsManaged,
|
bool IsManaged,
|
||||||
bool IsOnSecretsManagerStandalone,
|
bool IsOnSecretsManagerStandalone,
|
||||||
bool IsSubscriptionUnpaid);
|
bool IsSubscriptionUnpaid,
|
||||||
|
bool HasSubscription);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
|
||||||
using Bit.Core.Billing.Entities;
|
using Bit.Core.Billing.Entities;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
@ -12,18 +11,10 @@ namespace Bit.Core.Billing.Services;
|
|||||||
public interface IProviderBillingService
|
public interface IProviderBillingService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Assigns a specified number of <paramref name="seats"/> to a client <paramref name="organization"/> on behalf of
|
/// Changes the assigned provider plan for the provider.
|
||||||
/// its <paramref name="provider"/>. Seat adjustments for the client organization may autoscale the provider's Stripe
|
|
||||||
/// <see cref="Stripe.Subscription"/> depending on the provider's seat minimum for the client <paramref name="organization"/>'s
|
|
||||||
/// <see cref="PlanType"/>.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="provider">The <see cref="Provider"/> that manages the client <paramref name="organization"/>.</param>
|
/// <param name="command">The command to change the provider plan.</param>
|
||||||
/// <param name="organization">The client <see cref="Organization"/> whose <paramref name="seats"/> you want to update.</param>
|
Task ChangePlan(ChangeProviderPlanCommand command);
|
||||||
/// <param name="seats">The number of seats to assign to the client organization.</param>
|
|
||||||
Task AssignSeatsToClientOrganization(
|
|
||||||
Provider provider,
|
|
||||||
Organization organization,
|
|
||||||
int seats);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a Stripe <see cref="Stripe.Customer"/> for the provided client <paramref name="organization"/> utilizing
|
/// Create a Stripe <see cref="Stripe.Customer"/> for the provided client <paramref name="organization"/> utilizing
|
||||||
@ -44,18 +35,6 @@ public interface IProviderBillingService
|
|||||||
Task<byte[]> GenerateClientInvoiceReport(
|
Task<byte[]> GenerateClientInvoiceReport(
|
||||||
string invoiceId);
|
string invoiceId);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves the number of seats an MSP has assigned to its client organizations with a specified <paramref name="planType"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="providerId">The ID of the MSP to retrieve the assigned seat total for.</param>
|
|
||||||
/// <param name="planType">The type of plan to retrieve the assigned seat total for.</param>
|
|
||||||
/// <returns>An <see cref="int"/> representing the number of seats the provider has assigned to its client organizations with the specified <paramref name="planType"/>.</returns>
|
|
||||||
/// <exception cref="BillingException">Thrown when the provider represented by the <paramref name="providerId"/> is <see langword="null"/>.</exception>
|
|
||||||
/// <exception cref="BillingException">Thrown when the provider represented by the <paramref name="providerId"/> has <see cref="Provider.Type"/> <see cref="ProviderType.Reseller"/>.</exception>
|
|
||||||
Task<int> GetAssignedSeatTotalForPlanOrThrow(
|
|
||||||
Guid providerId,
|
|
||||||
PlanType planType);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Scales the <paramref name="provider"/>'s seats for the specified <paramref name="planType"/> using the provided <paramref name="seatAdjustment"/>.
|
/// Scales the <paramref name="provider"/>'s seats for the specified <paramref name="planType"/> using the provided <paramref name="seatAdjustment"/>.
|
||||||
/// This operation may autoscale the provider's Stripe <see cref="Stripe.Subscription"/> depending on the <paramref name="provider"/>'s seat minimum for the
|
/// This operation may autoscale the provider's Stripe <see cref="Stripe.Subscription"/> depending on the <paramref name="provider"/>'s seat minimum for the
|
||||||
@ -69,6 +48,22 @@ public interface IProviderBillingService
|
|||||||
PlanType planType,
|
PlanType planType,
|
||||||
int seatAdjustment);
|
int seatAdjustment);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the provided <paramref name="seatAdjustment"/> will result in a purchase for the <paramref name="provider"/>'s <see cref="planType"/>.
|
||||||
|
/// Seat adjustments that result in purchases include:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>The <paramref name="provider"/> going from below the seat minimum to above the seat minimum for the provided <paramref name="planType"/></item>
|
||||||
|
/// <item>The <paramref name="provider"/> going from above the seat minimum to further above the seat minimum for the provided <paramref name="planType"/></item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="provider">The provider to check seat adjustments for.</param>
|
||||||
|
/// <param name="planType">The plan type to check seat adjustments for.</param>
|
||||||
|
/// <param name="seatAdjustment">The change in seats for the <paramref name="provider"/>'s <paramref name="planType"/>.</param>
|
||||||
|
Task<bool> SeatAdjustmentResultsInPurchase(
|
||||||
|
Provider provider,
|
||||||
|
PlanType planType,
|
||||||
|
int seatAdjustment);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// For use during the provider setup process, this method creates a Stripe <see cref="Stripe.Customer"/> for the specified <paramref name="provider"/> utilizing the provided <paramref name="taxInfo"/>.
|
/// For use during the provider setup process, this method creates a Stripe <see cref="Stripe.Customer"/> for the specified <paramref name="provider"/> utilizing the provided <paramref name="taxInfo"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -90,12 +85,5 @@ public interface IProviderBillingService
|
|||||||
Task<Subscription> SetupSubscription(
|
Task<Subscription> SetupSubscription(
|
||||||
Provider provider);
|
Provider provider);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Changes the assigned provider plan for the provider.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="command">The command to change the provider plan.</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
Task ChangePlan(ChangeProviderPlanCommand command);
|
|
||||||
|
|
||||||
Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command);
|
Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command);
|
||||||
}
|
}
|
||||||
|
@ -62,18 +62,25 @@ public class OrganizationBillingService(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isEligibleForSelfHost = IsEligibleForSelfHost(organization);
|
||||||
|
var isManaged = organization.Status == OrganizationStatusType.Managed;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
||||||
|
{
|
||||||
|
return new OrganizationMetadata(isEligibleForSelfHost, isManaged, false,
|
||||||
|
false, false);
|
||||||
|
}
|
||||||
|
|
||||||
var customer = await subscriberService.GetCustomer(organization,
|
var customer = await subscriberService.GetCustomer(organization,
|
||||||
new CustomerGetOptions { Expand = ["discount.coupon.applies_to"] });
|
new CustomerGetOptions { Expand = ["discount.coupon.applies_to"] });
|
||||||
|
|
||||||
var subscription = await subscriberService.GetSubscription(organization);
|
var subscription = await subscriberService.GetSubscription(organization);
|
||||||
|
|
||||||
var isEligibleForSelfHost = IsEligibleForSelfHost(organization);
|
|
||||||
var isManaged = organization.Status == OrganizationStatusType.Managed;
|
|
||||||
var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription);
|
var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription);
|
||||||
var isSubscriptionUnpaid = IsSubscriptionUnpaid(subscription);
|
var isSubscriptionUnpaid = IsSubscriptionUnpaid(subscription);
|
||||||
|
var hasSubscription = true;
|
||||||
|
|
||||||
return new OrganizationMetadata(isEligibleForSelfHost, isManaged, isOnSecretsManagerStandalone,
|
return new OrganizationMetadata(isEligibleForSelfHost, isManaged, isOnSecretsManagerStandalone,
|
||||||
isSubscriptionUnpaid);
|
isSubscriptionUnpaid, hasSubscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdatePaymentMethod(
|
public async Task UpdatePaymentMethod(
|
||||||
|
@ -107,11 +107,9 @@ public static class FeatureFlagKeys
|
|||||||
public const string ItemShare = "item-share";
|
public const string ItemShare = "item-share";
|
||||||
public const string DuoRedirect = "duo-redirect";
|
public const string DuoRedirect = "duo-redirect";
|
||||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||||
public const string EnableConsolidatedBilling = "enable-consolidated-billing";
|
|
||||||
public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section";
|
public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section";
|
||||||
public const string EmailVerification = "email-verification";
|
public const string EmailVerification = "email-verification";
|
||||||
public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays";
|
public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays";
|
||||||
public const string AnhFcmv1Migration = "anh-fcmv1-migration";
|
|
||||||
public const string ExtensionRefresh = "extension-refresh";
|
public const string ExtensionRefresh = "extension-refresh";
|
||||||
public const string RestrictProviderAccess = "restrict-provider-access";
|
public const string RestrictProviderAccess = "restrict-provider-access";
|
||||||
public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service";
|
public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service";
|
||||||
@ -126,7 +124,6 @@ public static class FeatureFlagKeys
|
|||||||
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
|
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
|
||||||
public const string SSHAgent = "ssh-agent";
|
public const string SSHAgent = "ssh-agent";
|
||||||
public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token";
|
public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token";
|
||||||
public const string EnableUpgradePasswordManagerSub = "AC-2708-upgrade-password-manager-sub";
|
|
||||||
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
||||||
public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh";
|
public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh";
|
||||||
public const string GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor";
|
public const string GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor";
|
||||||
@ -143,6 +140,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string StorageReseedRefactor = "storage-reseed-refactor";
|
public const string StorageReseedRefactor = "storage-reseed-refactor";
|
||||||
public const string TrialPayment = "PM-8163-trial-payment";
|
public const string TrialPayment = "PM-8163-trial-payment";
|
||||||
public const string RemoveServerVersionHeader = "remove-server-version-header";
|
public const string RemoveServerVersionHeader = "remove-server-version-header";
|
||||||
|
public const string SecureOrgGroupDetails = "pm-3479-secure-org-group-details";
|
||||||
public const string AccessIntelligence = "pm-13227-access-intelligence";
|
public const string AccessIntelligence = "pm-13227-access-intelligence";
|
||||||
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
||||||
public const string PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises";
|
public const string PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises";
|
||||||
@ -155,6 +153,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss";
|
public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss";
|
||||||
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
|
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
|
||||||
public const string SecurityTasks = "security-tasks";
|
public const string SecurityTasks = "security-tasks";
|
||||||
|
public const string PM14401_ScaleMSPOnClientOrganizationUpdate = "PM-14401-scale-msp-on-client-organization-update";
|
||||||
|
|
||||||
public static List<string> GetAllKeys()
|
public static List<string> GetAllKeys()
|
||||||
{
|
{
|
||||||
|
@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Context;
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Identity;
|
using Bit.Core.Identity;
|
||||||
@ -363,9 +364,9 @@ public class CurrentContext : ICurrentContext
|
|||||||
|
|
||||||
public async Task<bool> ViewSubscription(Guid orgId)
|
public async Task<bool> ViewSubscription(Guid orgId)
|
||||||
{
|
{
|
||||||
var orgManagedByMspProvider = (await GetOrganizationProviderDetails()).Any(po => po.OrganizationId == orgId && po.ProviderType == ProviderType.Msp);
|
var isManagedByBillableProvider = (await GetOrganizationProviderDetails()).Any(po => po.OrganizationId == orgId && po.ProviderType.SupportsConsolidatedBilling());
|
||||||
|
|
||||||
return orgManagedByMspProvider
|
return isManagedByBillableProvider
|
||||||
? await ProviderUserForOrgAsync(orgId)
|
? await ProviderUserForOrgAsync(orgId)
|
||||||
: await OrganizationOwner(orgId);
|
: await OrganizationOwner(orgId);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ using Bit.Core.Repositories;
|
|||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Microsoft.Azure.NotificationHubs;
|
using Microsoft.Azure.NotificationHubs;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Bit.Core.NotificationHub;
|
namespace Bit.Core.NotificationHub;
|
||||||
@ -60,21 +59,10 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
|||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case DeviceType.Android:
|
case DeviceType.Android:
|
||||||
var featureService = _serviceProvider.GetRequiredService<IFeatureService>();
|
payloadTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\",\"payload\":\"$(payload)\"}}}";
|
||||||
if (featureService.IsEnabled(FeatureFlagKeys.AnhFcmv1Migration))
|
messageTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\"}," +
|
||||||
{
|
"\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}";
|
||||||
payloadTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\",\"payload\":\"$(payload)\"}}}";
|
installation.Platform = NotificationPlatform.FcmV1;
|
||||||
messageTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\"}," +
|
|
||||||
"\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}";
|
|
||||||
installation.Platform = NotificationPlatform.FcmV1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
payloadTemplate = "{\"data\":{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}}}";
|
|
||||||
messageTemplate = "{\"data\":{\"data\":{\"type\":\"#(type)\"}," +
|
|
||||||
"\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}";
|
|
||||||
installation.Platform = NotificationPlatform.Fcm;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case DeviceType.iOS:
|
case DeviceType.iOS:
|
||||||
payloadTemplate = "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}," +
|
payloadTemplate = "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}," +
|
||||||
|
20
src/Core/Vault/Entities/SecurityTask.cs
Normal file
20
src/Core/Vault/Entities/SecurityTask.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Vault.Entities;
|
||||||
|
|
||||||
|
public class SecurityTask : ITableObject<Guid>
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
public Guid? CipherId { get; set; }
|
||||||
|
public Enums.SecurityTaskType Type { get; set; }
|
||||||
|
public Enums.SecurityTaskStatus Status { get; set; }
|
||||||
|
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
public void SetNewId()
|
||||||
|
{
|
||||||
|
Id = CoreHelpers.GenerateComb();
|
||||||
|
}
|
||||||
|
}
|
18
src/Core/Vault/Enums/SecurityTaskStatus.cs
Normal file
18
src/Core/Vault/Enums/SecurityTaskStatus.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Bit.Core.Vault.Enums;
|
||||||
|
|
||||||
|
public enum SecurityTaskStatus : byte
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default status for newly created tasks that have not been completed.
|
||||||
|
/// </summary>
|
||||||
|
[Display(Name = "Pending")]
|
||||||
|
Pending = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Status when a task is considered complete and has no remaining actions
|
||||||
|
/// </summary>
|
||||||
|
[Display(Name = "Completed")]
|
||||||
|
Completed = 1,
|
||||||
|
}
|
12
src/Core/Vault/Enums/SecurityTaskType.cs
Normal file
12
src/Core/Vault/Enums/SecurityTaskType.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Bit.Core.Vault.Enums;
|
||||||
|
|
||||||
|
public enum SecurityTaskType : byte
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Task to update a cipher's password that was found to be at-risk by an administrator
|
||||||
|
/// </summary>
|
||||||
|
[Display(Name = "Update at-risk credential")]
|
||||||
|
UpdateAtRiskCredential = 0
|
||||||
|
}
|
9
src/Core/Vault/Repositories/ISecurityTaskRepository.cs
Normal file
9
src/Core/Vault/Repositories/ISecurityTaskRepository.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Vault.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Vault.Repositories;
|
||||||
|
|
||||||
|
public interface ISecurityTaskRepository : IRepository<SecurityTask, Guid>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
@ -1,9 +1,7 @@
|
|||||||
|
using System.Text.Json;
|
||||||
using System.Text.Json;
|
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Identity;
|
using Bit.Core.Auth.Identity.TokenProviders;
|
||||||
using Bit.Core.Auth.Models;
|
using Bit.Core.Auth.Models;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -52,8 +50,7 @@ public interface ITwoFactorAuthenticationValidator
|
|||||||
public class TwoFactorAuthenticationValidator(
|
public class TwoFactorAuthenticationValidator(
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
UserManager<User> userManager,
|
UserManager<User> userManager,
|
||||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider,
|
||||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
@ -63,8 +60,7 @@ public class TwoFactorAuthenticationValidator(
|
|||||||
{
|
{
|
||||||
private readonly IUserService _userService = userService;
|
private readonly IUserService _userService = userService;
|
||||||
private readonly UserManager<User> _userManager = userManager;
|
private readonly UserManager<User> _userManager = userManager;
|
||||||
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider = organizationDuoWebTokenProvider;
|
private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider = organizationDuoWebTokenProvider;
|
||||||
private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService = duoWebV4SDKService;
|
|
||||||
private readonly IFeatureService _featureService = featureService;
|
private readonly IFeatureService _featureService = featureService;
|
||||||
private readonly IApplicationCacheService _applicationCacheService = applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService = applicationCacheService;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository = organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository = organizationUserRepository;
|
||||||
@ -153,17 +149,7 @@ public class TwoFactorAuthenticationValidator(
|
|||||||
{
|
{
|
||||||
if (organization.TwoFactorProviderIsEnabled(type))
|
if (organization.TwoFactorProviderIsEnabled(type))
|
||||||
{
|
{
|
||||||
// DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt
|
return await _organizationDuoUniversalTokenProvider.ValidateAsync(token, organization, user);
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect))
|
|
||||||
{
|
|
||||||
if (!token.Contains(':'))
|
|
||||||
{
|
|
||||||
// We have to send the provider to the DuoWebV4SDKService to create the DuoClient
|
|
||||||
var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
|
|
||||||
return await _duoWebV4SDKService.ValidateAsync(token, provider, user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user);
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -181,19 +167,6 @@ public class TwoFactorAuthenticationValidator(
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect))
|
|
||||||
{
|
|
||||||
if (type == TwoFactorProviderType.Duo)
|
|
||||||
{
|
|
||||||
if (!token.Contains(':'))
|
|
||||||
{
|
|
||||||
// We have to send the provider to the DuoWebV4SDKService to create the DuoClient
|
|
||||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
|
|
||||||
return await _duoWebV4SDKService.ValidateAsync(token, provider, user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return await _userManager.VerifyTwoFactorTokenAsync(user,
|
return await _userManager.VerifyTwoFactorTokenAsync(user,
|
||||||
CoreHelpers.CustomProviderName(type), token);
|
CoreHelpers.CustomProviderName(type), token);
|
||||||
default:
|
default:
|
||||||
@ -248,10 +221,11 @@ public class TwoFactorAuthenticationValidator(
|
|||||||
in the future the `AuthUrl` will be the generated "token" - PM-8107
|
in the future the `AuthUrl` will be the generated "token" - PM-8107
|
||||||
*/
|
*/
|
||||||
if (type == TwoFactorProviderType.OrganizationDuo &&
|
if (type == TwoFactorProviderType.OrganizationDuo &&
|
||||||
await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization))
|
await _organizationDuoUniversalTokenProvider.CanGenerateTwoFactorTokenAsync(organization))
|
||||||
{
|
{
|
||||||
twoFactorParams.Add("Host", provider.MetaData["Host"]);
|
twoFactorParams.Add("Host", provider.MetaData["Host"]);
|
||||||
twoFactorParams.Add("AuthUrl", await _duoWebV4SDKService.GenerateAsync(provider, user));
|
twoFactorParams.Add("AuthUrl",
|
||||||
|
await _organizationDuoUniversalTokenProvider.GenerateAsync(organization, user));
|
||||||
|
|
||||||
return twoFactorParams;
|
return twoFactorParams;
|
||||||
}
|
}
|
||||||
@ -261,13 +235,9 @@ public class TwoFactorAuthenticationValidator(
|
|||||||
CoreHelpers.CustomProviderName(type));
|
CoreHelpers.CustomProviderName(type));
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
/*
|
|
||||||
Note: Duo is in the midst of being updated to use the UserManager built-in TwoFactor class
|
|
||||||
in the future the `AuthUrl` will be the generated "token" - PM-8107
|
|
||||||
*/
|
|
||||||
case TwoFactorProviderType.Duo:
|
case TwoFactorProviderType.Duo:
|
||||||
twoFactorParams.Add("Host", provider.MetaData["Host"]);
|
twoFactorParams.Add("Host", provider.MetaData["Host"]);
|
||||||
twoFactorParams.Add("AuthUrl", await _duoWebV4SDKService.GenerateAsync(provider, user));
|
twoFactorParams.Add("AuthUrl", token);
|
||||||
break;
|
break;
|
||||||
case TwoFactorProviderType.WebAuthn:
|
case TwoFactorProviderType.WebAuthn:
|
||||||
if (token != null)
|
if (token != null)
|
||||||
|
@ -59,6 +59,7 @@ public static class DapperServiceCollectionExtensions
|
|||||||
services
|
services
|
||||||
.AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();
|
.AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();
|
||||||
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
|
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
|
||||||
|
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
|
||||||
|
|
||||||
if (selfHosted)
|
if (selfHosted)
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Vault.Entities;
|
||||||
|
using Bit.Core.Vault.Repositories;
|
||||||
|
using Bit.Infrastructure.Dapper.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.Dapper.Vault.Repositories;
|
||||||
|
|
||||||
|
public class SecurityTaskRepository : Repository<SecurityTask, Guid>, ISecurityTaskRepository
|
||||||
|
{
|
||||||
|
public SecurityTaskRepository(GlobalSettings globalSettings)
|
||||||
|
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public SecurityTaskRepository(string connectionString, string readOnlyConnectionString)
|
||||||
|
: base(connectionString, readOnlyConnectionString)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
}
|
@ -96,6 +96,7 @@ public static class EntityFrameworkServiceCollectionExtensions
|
|||||||
services
|
services
|
||||||
.AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();
|
.AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();
|
||||||
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
|
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
|
||||||
|
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
|
||||||
|
|
||||||
if (selfHosted)
|
if (selfHosted)
|
||||||
{
|
{
|
||||||
|
@ -77,6 +77,7 @@ public class DatabaseContext : DbContext
|
|||||||
public DbSet<NotificationStatus> NotificationStatuses { get; set; }
|
public DbSet<NotificationStatus> NotificationStatuses { get; set; }
|
||||||
public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; }
|
public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; }
|
||||||
public DbSet<PasswordHealthReportApplication> PasswordHealthReportApplications { get; set; }
|
public DbSet<PasswordHealthReportApplication> PasswordHealthReportApplications { get; set; }
|
||||||
|
public DbSet<SecurityTask> SecurityTasks { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
using Bit.Infrastructure.EntityFramework.Vault.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework.Vault.Configurations;
|
||||||
|
|
||||||
|
public class SecurityTaskEntityTypeConfiguration : IEntityTypeConfiguration<SecurityTask>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<SecurityTask> builder)
|
||||||
|
{
|
||||||
|
builder
|
||||||
|
.Property(s => s.Id)
|
||||||
|
.ValueGeneratedNever();
|
||||||
|
|
||||||
|
builder
|
||||||
|
.HasKey(s => s.Id)
|
||||||
|
.IsClustered();
|
||||||
|
|
||||||
|
builder
|
||||||
|
.HasIndex(s => s.OrganizationId)
|
||||||
|
.IsClustered(false);
|
||||||
|
|
||||||
|
builder
|
||||||
|
.HasIndex(s => s.CipherId)
|
||||||
|
.IsClustered(false);
|
||||||
|
|
||||||
|
builder
|
||||||
|
.ToTable(nameof(SecurityTask));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework.Vault.Models;
|
||||||
|
|
||||||
|
public class SecurityTask : Core.Vault.Entities.SecurityTask
|
||||||
|
{
|
||||||
|
public virtual Organization Organization { get; set; }
|
||||||
|
public virtual Cipher Cipher { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SecurityTaskMapperProfile : Profile
|
||||||
|
{
|
||||||
|
public SecurityTaskMapperProfile()
|
||||||
|
{
|
||||||
|
CreateMap<Core.Vault.Entities.SecurityTask, SecurityTask>().ReverseMap();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Bit.Core.Vault.Repositories;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Vault.Models;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework.Vault.Repositories;
|
||||||
|
|
||||||
|
public class SecurityTaskRepository : Repository<Core.Vault.Entities.SecurityTask, SecurityTask, Guid>, ISecurityTaskRepository
|
||||||
|
{
|
||||||
|
public SecurityTaskRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
|
||||||
|
: base(serviceScopeFactory, mapper, (context) => context.SecurityTasks)
|
||||||
|
{ }
|
||||||
|
}
|
@ -10,6 +10,7 @@ using Bit.Core.AdminConsole.Services.Implementations;
|
|||||||
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Identity;
|
using Bit.Core.Auth.Identity;
|
||||||
|
using Bit.Core.Auth.Identity.TokenProviders;
|
||||||
using Bit.Core.Auth.IdentityServer;
|
using Bit.Core.Auth.IdentityServer;
|
||||||
using Bit.Core.Auth.LoginFeatures;
|
using Bit.Core.Auth.LoginFeatures;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
@ -113,6 +114,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddSingleton<IDeviceService, DeviceService>();
|
services.AddSingleton<IDeviceService, DeviceService>();
|
||||||
services.AddScoped<ISsoConfigService, SsoConfigService>();
|
services.AddScoped<ISsoConfigService, SsoConfigService>();
|
||||||
services.AddScoped<IAuthRequestService, AuthRequestService>();
|
services.AddScoped<IAuthRequestService, AuthRequestService>();
|
||||||
|
services.AddScoped<IDuoUniversalTokenService, DuoUniversalTokenService>();
|
||||||
services.AddScoped<ISendService, SendService>();
|
services.AddScoped<ISendService, SendService>();
|
||||||
services.AddLoginServices();
|
services.AddLoginServices();
|
||||||
services.AddScoped<IOrganizationDomainService, OrganizationDomainService>();
|
services.AddScoped<IOrganizationDomainService, OrganizationDomainService>();
|
||||||
@ -388,8 +390,7 @@ public static class ServiceCollectionExtensions
|
|||||||
public static IdentityBuilder AddCustomIdentityServices(
|
public static IdentityBuilder AddCustomIdentityServices(
|
||||||
this IServiceCollection services, GlobalSettings globalSettings)
|
this IServiceCollection services, GlobalSettings globalSettings)
|
||||||
{
|
{
|
||||||
services.AddScoped<IOrganizationDuoWebTokenProvider, OrganizationDuoWebTokenProvider>();
|
services.AddScoped<IOrganizationDuoUniversalTokenProvider, OrganizationDuoUniversalTokenProvider>();
|
||||||
services.AddScoped<ITemporaryDuoWebV4SDKService, TemporaryDuoWebV4SDKService>();
|
|
||||||
services.Configure<PasswordHasherOptions>(options => options.IterationCount = 100000);
|
services.Configure<PasswordHasherOptions>(options => options.IterationCount = 100000);
|
||||||
services.Configure<TwoFactorRememberTokenProviderOptions>(options =>
|
services.Configure<TwoFactorRememberTokenProviderOptions>(options =>
|
||||||
{
|
{
|
||||||
@ -430,7 +431,7 @@ public static class ServiceCollectionExtensions
|
|||||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email))
|
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email))
|
||||||
.AddTokenProvider<YubicoOtpTokenProvider>(
|
.AddTokenProvider<YubicoOtpTokenProvider>(
|
||||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.YubiKey))
|
CoreHelpers.CustomProviderName(TwoFactorProviderType.YubiKey))
|
||||||
.AddTokenProvider<DuoWebTokenProvider>(
|
.AddTokenProvider<DuoUniversalTokenProvider>(
|
||||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Duo))
|
CoreHelpers.CustomProviderName(TwoFactorProviderType.Duo))
|
||||||
.AddTokenProvider<TwoFactorRememberTokenProvider>(
|
.AddTokenProvider<TwoFactorRememberTokenProvider>(
|
||||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember))
|
CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember))
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user