mirror of
https://github.com/bitwarden/server.git
synced 2025-04-17 11:08:16 -05:00
Merge branch 'main' into ac/pm-15621/refactor-delete-command
This commit is contained in:
commit
48ced72b0b
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -314,7 +314,7 @@ jobs:
|
|||||||
output-format: sarif
|
output-format: sarif
|
||||||
|
|
||||||
- name: Upload Grype results to GitHub
|
- name: Upload Grype results to GitHub
|
||||||
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
|
||||||
with:
|
with:
|
||||||
sarif_file: ${{ steps.container-scan.outputs.sarif }}
|
sarif_file: ${{ steps.container-scan.outputs.sarif }}
|
||||||
|
|
||||||
|
4
.github/workflows/scan.yml
vendored
4
.github/workflows/scan.yml
vendored
@ -46,7 +46,7 @@ jobs:
|
|||||||
--output-path . ${{ env.INCREMENTAL }}
|
--output-path . ${{ env.INCREMENTAL }}
|
||||||
|
|
||||||
- name: Upload Checkmarx results to GitHub
|
- name: Upload Checkmarx results to GitHub
|
||||||
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
|
||||||
with:
|
with:
|
||||||
sarif_file: cx_result.sarif
|
sarif_file: cx_result.sarif
|
||||||
|
|
||||||
@ -85,6 +85,6 @@ jobs:
|
|||||||
/d:sonar.test.inclusions=test/,bitwarden_license/test/ \
|
/d:sonar.test.inclusions=test/,bitwarden_license/test/ \
|
||||||
/d:sonar.exclusions=test/,bitwarden_license/test/ \
|
/d:sonar.exclusions=test/,bitwarden_license/test/ \
|
||||||
/o:"${{ github.repository_owner }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" \
|
/o:"${{ github.repository_owner }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" \
|
||||||
/d:sonar.host.url="https://sonarcloud.io"
|
/d:sonar.host.url="https://sonarcloud.io" ${{ contains(github.event_name, 'pull_request') && format('/d:sonar.pullrequest.key={0}', github.event.pull_request.number) || '' }}
|
||||||
dotnet build
|
dotnet build
|
||||||
dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
|
dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
|
||||||
|
@ -125,6 +125,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Notifications.Test", "test\
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.Dapper.Test", "test\Infrastructure.Dapper.Test\Infrastructure.Dapper.Test.csproj", "{4A725DB3-BE4F-4C23-9087-82D0610D67AF}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.Dapper.Test", "test\Infrastructure.Dapper.Test\Infrastructure.Dapper.Test.csproj", "{4A725DB3-BE4F-4C23-9087-82D0610D67AF}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "test\Events.IntegrationTest\Events.IntegrationTest.csproj", "{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -313,6 +315,10 @@ Global
|
|||||||
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.Build.0 = Release|Any CPU
|
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -363,6 +369,7 @@ Global
|
|||||||
{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
{90D85D8F-5577-4570-A96E-5A2E185F0F6F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{90D85D8F-5577-4570-A96E-5A2E185F0F6F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
{4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
|
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Bit.Commercial.Core.Billing.Models;
|
using Bit.Commercial.Core.Billing.Models;
|
||||||
|
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.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.Models;
|
||||||
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;
|
||||||
@ -24,6 +27,7 @@ using Stripe;
|
|||||||
namespace Bit.Commercial.Core.Billing;
|
namespace Bit.Commercial.Core.Billing;
|
||||||
|
|
||||||
public class ProviderBillingService(
|
public class ProviderBillingService(
|
||||||
|
IEventService eventService,
|
||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
ILogger<ProviderBillingService> logger,
|
ILogger<ProviderBillingService> logger,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@ -31,10 +35,93 @@ public class ProviderBillingService(
|
|||||||
IProviderInvoiceItemRepository providerInvoiceItemRepository,
|
IProviderInvoiceItemRepository providerInvoiceItemRepository,
|
||||||
IProviderOrganizationRepository providerOrganizationRepository,
|
IProviderOrganizationRepository providerOrganizationRepository,
|
||||||
IProviderPlanRepository providerPlanRepository,
|
IProviderPlanRepository providerPlanRepository,
|
||||||
|
IProviderUserRepository providerUserRepository,
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
ISubscriberService subscriberService,
|
ISubscriberService subscriberService,
|
||||||
ITaxService taxService) : IProviderBillingService
|
ITaxService taxService) : IProviderBillingService
|
||||||
{
|
{
|
||||||
|
[RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)]
|
||||||
|
public async Task AddExistingOrganization(
|
||||||
|
Provider provider,
|
||||||
|
Organization organization,
|
||||||
|
string key)
|
||||||
|
{
|
||||||
|
await stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
|
||||||
|
new SubscriptionUpdateOptions
|
||||||
|
{
|
||||||
|
CancelAtPeriodEnd = false
|
||||||
|
});
|
||||||
|
|
||||||
|
var subscription =
|
||||||
|
await stripeAdapter.SubscriptionCancelAsync(organization.GatewaySubscriptionId,
|
||||||
|
new SubscriptionCancelOptions
|
||||||
|
{
|
||||||
|
CancellationDetails = new SubscriptionCancellationDetailsOptions
|
||||||
|
{
|
||||||
|
Comment = $"Organization was added to Provider with ID {provider.Id}"
|
||||||
|
},
|
||||||
|
InvoiceNow = true,
|
||||||
|
Prorate = true,
|
||||||
|
Expand = ["latest_invoice", "test_clock"]
|
||||||
|
});
|
||||||
|
|
||||||
|
var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow;
|
||||||
|
|
||||||
|
var wasTrialing = subscription.TrialEnd.HasValue && subscription.TrialEnd.Value > now;
|
||||||
|
|
||||||
|
if (!wasTrialing && subscription.LatestInvoice.Status == StripeConstants.InvoiceStatus.Draft)
|
||||||
|
{
|
||||||
|
await stripeAdapter.InvoiceFinalizeInvoiceAsync(subscription.LatestInvoiceId,
|
||||||
|
new InvoiceFinalizeOptions { AutoAdvance = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
var managedPlanType = await GetManagedPlanTypeAsync(provider, organization);
|
||||||
|
|
||||||
|
// TODO: Replace with PricingClient
|
||||||
|
var plan = StaticStore.GetPlan(managedPlanType);
|
||||||
|
organization.Plan = plan.Name;
|
||||||
|
organization.PlanType = plan.Type;
|
||||||
|
organization.MaxCollections = plan.PasswordManager.MaxCollections;
|
||||||
|
organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb;
|
||||||
|
organization.UsePolicies = plan.HasPolicies;
|
||||||
|
organization.UseSso = plan.HasSso;
|
||||||
|
organization.UseGroups = plan.HasGroups;
|
||||||
|
organization.UseEvents = plan.HasEvents;
|
||||||
|
organization.UseDirectory = plan.HasDirectory;
|
||||||
|
organization.UseTotp = plan.HasTotp;
|
||||||
|
organization.Use2fa = plan.Has2fa;
|
||||||
|
organization.UseApi = plan.HasApi;
|
||||||
|
organization.UseResetPassword = plan.HasResetPassword;
|
||||||
|
organization.SelfHost = plan.HasSelfHost;
|
||||||
|
organization.UsersGetPremium = plan.UsersGetPremium;
|
||||||
|
organization.UseCustomPermissions = plan.HasCustomPermissions;
|
||||||
|
organization.UseScim = plan.HasScim;
|
||||||
|
organization.UseKeyConnector = plan.HasKeyConnector;
|
||||||
|
organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb;
|
||||||
|
organization.BillingEmail = provider.BillingEmail!;
|
||||||
|
organization.GatewaySubscriptionId = null;
|
||||||
|
organization.ExpirationDate = null;
|
||||||
|
organization.MaxAutoscaleSeats = null;
|
||||||
|
organization.Status = OrganizationStatusType.Managed;
|
||||||
|
|
||||||
|
var providerOrganization = new ProviderOrganization
|
||||||
|
{
|
||||||
|
ProviderId = provider.Id,
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
Key = key
|
||||||
|
};
|
||||||
|
|
||||||
|
await Task.WhenAll(
|
||||||
|
organizationRepository.ReplaceAsync(organization),
|
||||||
|
providerOrganizationRepository.CreateAsync(providerOrganization),
|
||||||
|
ScaleSeats(provider, organization.PlanType, organization.Seats!.Value)
|
||||||
|
);
|
||||||
|
|
||||||
|
await eventService.LogProviderOrganizationEventAsync(
|
||||||
|
providerOrganization,
|
||||||
|
EventType.ProviderOrganization_Added);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ChangePlan(ChangeProviderPlanCommand command)
|
public async Task ChangePlan(ChangeProviderPlanCommand command)
|
||||||
{
|
{
|
||||||
var plan = await providerPlanRepository.GetByIdAsync(command.ProviderPlanId);
|
var plan = await providerPlanRepository.GetByIdAsync(command.ProviderPlanId);
|
||||||
@ -206,6 +293,81 @@ public class ProviderBillingService(
|
|||||||
return memoryStream.ToArray();
|
return memoryStream.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)]
|
||||||
|
public async Task<IEnumerable<AddableOrganization>> GetAddableOrganizations(
|
||||||
|
Provider provider,
|
||||||
|
Guid userId)
|
||||||
|
{
|
||||||
|
var providerUser = await providerUserRepository.GetByProviderUserAsync(provider.Id, userId);
|
||||||
|
|
||||||
|
if (providerUser is not { Status: ProviderUserStatusType.Confirmed })
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidates = await organizationRepository.GetAddableToProviderByUserIdAsync(userId, provider.Type);
|
||||||
|
|
||||||
|
var active = (await Task.WhenAll(candidates.Select(async organization =>
|
||||||
|
{
|
||||||
|
var subscription = await subscriberService.GetSubscription(organization);
|
||||||
|
return (organization, subscription);
|
||||||
|
})))
|
||||||
|
.Where(pair => pair.subscription is
|
||||||
|
{
|
||||||
|
Status:
|
||||||
|
StripeConstants.SubscriptionStatus.Active or
|
||||||
|
StripeConstants.SubscriptionStatus.Trialing or
|
||||||
|
StripeConstants.SubscriptionStatus.PastDue
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
if (active.Count == 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Task.WhenAll(active.Select(async pair =>
|
||||||
|
{
|
||||||
|
var (organization, _) = pair;
|
||||||
|
|
||||||
|
var planName = DerivePlanName(provider, organization);
|
||||||
|
|
||||||
|
var addable = new AddableOrganization(
|
||||||
|
organization.Id,
|
||||||
|
organization.Name,
|
||||||
|
planName,
|
||||||
|
organization.Seats!.Value);
|
||||||
|
|
||||||
|
if (providerUser.Type != ProviderUserType.ServiceUser)
|
||||||
|
{
|
||||||
|
return addable;
|
||||||
|
}
|
||||||
|
|
||||||
|
var applicablePlanType = await GetManagedPlanTypeAsync(provider, organization);
|
||||||
|
|
||||||
|
var requiresPurchase =
|
||||||
|
await SeatAdjustmentResultsInPurchase(provider, applicablePlanType, organization.Seats!.Value);
|
||||||
|
|
||||||
|
return addable with { Disabled = requiresPurchase };
|
||||||
|
}));
|
||||||
|
|
||||||
|
string DerivePlanName(Provider localProvider, Organization localOrganization)
|
||||||
|
{
|
||||||
|
if (localProvider.Type == ProviderType.Msp)
|
||||||
|
{
|
||||||
|
return localOrganization.PlanType switch
|
||||||
|
{
|
||||||
|
var planType when PlanConstants.EnterprisePlanTypes.Contains(planType) => "Enterprise",
|
||||||
|
var planType when PlanConstants.TeamsPlanTypes.Contains(planType) => "Teams",
|
||||||
|
_ => throw new BillingException()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Replace with PricingClient
|
||||||
|
var plan = StaticStore.GetPlan(localOrganization.PlanType);
|
||||||
|
return plan.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ScaleSeats(
|
public async Task ScaleSeats(
|
||||||
Provider provider,
|
Provider provider,
|
||||||
PlanType planType,
|
PlanType planType,
|
||||||
@ -582,4 +744,21 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
return providerPlan;
|
return providerPlan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<PlanType> GetManagedPlanTypeAsync(
|
||||||
|
Provider provider,
|
||||||
|
Organization organization)
|
||||||
|
{
|
||||||
|
if (provider.Type == ProviderType.MultiOrganizationEnterprise)
|
||||||
|
{
|
||||||
|
return (await providerPlanRepository.GetByProviderId(provider.Id)).First().PlanType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return organization.PlanType switch
|
||||||
|
{
|
||||||
|
var planType when PlanConstants.TeamsPlanTypes.Contains(planType) => PlanType.TeamsMonthly,
|
||||||
|
var planType when PlanConstants.EnterprisePlanTypes.Contains(planType) => PlanType.EnterpriseMonthly,
|
||||||
|
_ => throw new BillingException()
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,3 +21,7 @@ IDP_SP_ACS_URL=http://localhost:51822/saml2/yourOrgIdHere/Acs
|
|||||||
# Should match server listen ports in reverse-proxy.conf
|
# Should match server listen ports in reverse-proxy.conf
|
||||||
API_PROXY_PORT=4100
|
API_PROXY_PORT=4100
|
||||||
IDENTITY_PROXY_PORT=33756
|
IDENTITY_PROXY_PORT=33756
|
||||||
|
|
||||||
|
# Optional RabbitMQ configuration
|
||||||
|
RABBITMQ_DEFAULT_USER=bitwarden
|
||||||
|
RABBITMQ_DEFAULT_PASS=SET_A_PASSWORD_HERE_123
|
||||||
|
@ -84,6 +84,20 @@ services:
|
|||||||
profiles:
|
profiles:
|
||||||
- idp
|
- idp
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
image: rabbitmq:management
|
||||||
|
container_name: rabbitmq
|
||||||
|
ports:
|
||||||
|
- "5672:5672"
|
||||||
|
- "15672:15672"
|
||||||
|
environment:
|
||||||
|
RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
|
||||||
|
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
|
||||||
|
volumes:
|
||||||
|
- rabbitmq_data:/var/lib/rabbitmq_data
|
||||||
|
profiles:
|
||||||
|
- rabbitmq
|
||||||
|
|
||||||
reverse-proxy:
|
reverse-proxy:
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
container_name: reverse-proxy
|
container_name: reverse-proxy
|
||||||
@ -99,3 +113,4 @@ volumes:
|
|||||||
mssql_dev_data:
|
mssql_dev_data:
|
||||||
postgres_dev_data:
|
postgres_dev_data:
|
||||||
mysql_dev_data:
|
mysql_dev_data:
|
||||||
|
rabbitmq_data:
|
||||||
|
@ -421,6 +421,11 @@ public class OrganizationsController : Controller
|
|||||||
|
|
||||||
private void UpdateOrganization(Organization organization, OrganizationEditModel model)
|
private void UpdateOrganization(Organization organization, OrganizationEditModel model)
|
||||||
{
|
{
|
||||||
|
if (_accessControlService.UserHasPermission(Permission.Org_Name_Edit))
|
||||||
|
{
|
||||||
|
organization.Name = WebUtility.HtmlEncode(model.Name);
|
||||||
|
}
|
||||||
|
|
||||||
if (_accessControlService.UserHasPermission(Permission.Org_CheckEnabledBox))
|
if (_accessControlService.UserHasPermission(Permission.Org_CheckEnabledBox))
|
||||||
{
|
{
|
||||||
organization.Enabled = model.Enabled;
|
organization.Enabled = model.Enabled;
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
var canViewBilling = AccessControlService.UserHasPermission(Permission.Org_Billing_View);
|
var canViewBilling = AccessControlService.UserHasPermission(Permission.Org_Billing_View);
|
||||||
var canViewPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_View);
|
var canViewPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_View);
|
||||||
var canViewLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_View);
|
var canViewLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_View);
|
||||||
|
var canEditName = AccessControlService.UserHasPermission(Permission.Org_Name_Edit);
|
||||||
var canCheckEnabled = AccessControlService.UserHasPermission(Permission.Org_CheckEnabledBox);
|
var canCheckEnabled = AccessControlService.UserHasPermission(Permission.Org_CheckEnabledBox);
|
||||||
var canEditPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_Edit);
|
var canEditPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_Edit);
|
||||||
var canEditLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_Edit);
|
var canEditLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_Edit);
|
||||||
@ -28,7 +29,7 @@
|
|||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-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 disabled="@(canEditName ? null : "disabled")">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,6 +22,7 @@ public enum Permission
|
|||||||
Org_List_View,
|
Org_List_View,
|
||||||
Org_OrgInformation_View,
|
Org_OrgInformation_View,
|
||||||
Org_GeneralDetails_View,
|
Org_GeneralDetails_View,
|
||||||
|
Org_Name_Edit,
|
||||||
Org_CheckEnabledBox,
|
Org_CheckEnabledBox,
|
||||||
Org_BusinessInformation_View,
|
Org_BusinessInformation_View,
|
||||||
Org_InitiateTrial,
|
Org_InitiateTrial,
|
||||||
|
@ -24,6 +24,7 @@ public static class RolePermissionMapping
|
|||||||
Permission.User_Billing_Edit,
|
Permission.User_Billing_Edit,
|
||||||
Permission.User_Billing_LaunchGateway,
|
Permission.User_Billing_LaunchGateway,
|
||||||
Permission.User_NewDeviceException_Edit,
|
Permission.User_NewDeviceException_Edit,
|
||||||
|
Permission.Org_Name_Edit,
|
||||||
Permission.Org_CheckEnabledBox,
|
Permission.Org_CheckEnabledBox,
|
||||||
Permission.Org_List_View,
|
Permission.Org_List_View,
|
||||||
Permission.Org_OrgInformation_View,
|
Permission.Org_OrgInformation_View,
|
||||||
@ -71,6 +72,7 @@ public static class RolePermissionMapping
|
|||||||
Permission.User_Billing_Edit,
|
Permission.User_Billing_Edit,
|
||||||
Permission.User_Billing_LaunchGateway,
|
Permission.User_Billing_LaunchGateway,
|
||||||
Permission.User_NewDeviceException_Edit,
|
Permission.User_NewDeviceException_Edit,
|
||||||
|
Permission.Org_Name_Edit,
|
||||||
Permission.Org_CheckEnabledBox,
|
Permission.Org_CheckEnabledBox,
|
||||||
Permission.Org_List_View,
|
Permission.Org_List_View,
|
||||||
Permission.Org_OrgInformation_View,
|
Permission.Org_OrgInformation_View,
|
||||||
@ -116,6 +118,7 @@ public static class RolePermissionMapping
|
|||||||
Permission.User_Billing_View,
|
Permission.User_Billing_View,
|
||||||
Permission.User_Billing_LaunchGateway,
|
Permission.User_Billing_LaunchGateway,
|
||||||
Permission.User_NewDeviceException_Edit,
|
Permission.User_NewDeviceException_Edit,
|
||||||
|
Permission.Org_Name_Edit,
|
||||||
Permission.Org_CheckEnabledBox,
|
Permission.Org_CheckEnabledBox,
|
||||||
Permission.Org_List_View,
|
Permission.Org_List_View,
|
||||||
Permission.Org_OrgInformation_View,
|
Permission.Org_OrgInformation_View,
|
||||||
@ -148,6 +151,7 @@ public static class RolePermissionMapping
|
|||||||
Permission.User_Billing_View,
|
Permission.User_Billing_View,
|
||||||
Permission.User_Billing_Edit,
|
Permission.User_Billing_Edit,
|
||||||
Permission.User_Billing_LaunchGateway,
|
Permission.User_Billing_LaunchGateway,
|
||||||
|
Permission.Org_Name_Edit,
|
||||||
Permission.Org_CheckEnabledBox,
|
Permission.Org_CheckEnabledBox,
|
||||||
Permission.Org_List_View,
|
Permission.Org_List_View,
|
||||||
Permission.Org_OrgInformation_View,
|
Permission.Org_OrgInformation_View,
|
||||||
@ -185,6 +189,7 @@ public static class RolePermissionMapping
|
|||||||
Permission.User_Premium_View,
|
Permission.User_Premium_View,
|
||||||
Permission.User_Licensing_View,
|
Permission.User_Licensing_View,
|
||||||
Permission.User_Licensing_Edit,
|
Permission.User_Licensing_Edit,
|
||||||
|
Permission.Org_Name_Edit,
|
||||||
Permission.Org_CheckEnabledBox,
|
Permission.Org_CheckEnabledBox,
|
||||||
Permission.Org_List_View,
|
Permission.Org_List_View,
|
||||||
Permission.Org_OrgInformation_View,
|
Permission.Org_OrgInformation_View,
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using Bit.Api.Billing.Models.Requests;
|
using Bit.Api.Billing.Controllers;
|
||||||
|
using Bit.Api.Billing.Models.Requests;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
@ -7,13 +9,15 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Bit.Api.Billing.Controllers;
|
namespace Bit.Api.AdminConsole.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,
|
||||||
@ -22,7 +26,10 @@ public class ProviderClientsController(
|
|||||||
IProviderService providerService,
|
IProviderService providerService,
|
||||||
IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService)
|
IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService)
|
||||||
{
|
{
|
||||||
|
private readonly ICurrentContext _currentContext = currentContext;
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<IResult> CreateAsync(
|
public async Task<IResult> CreateAsync(
|
||||||
[FromRoute] Guid providerId,
|
[FromRoute] Guid providerId,
|
||||||
[FromBody] CreateClientOrganizationRequestBody requestBody)
|
[FromBody] CreateClientOrganizationRequestBody requestBody)
|
||||||
@ -80,6 +87,7 @@ public class ProviderClientsController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{providerOrganizationId:guid}")]
|
[HttpPut("{providerOrganizationId:guid}")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<IResult> UpdateAsync(
|
public async Task<IResult> UpdateAsync(
|
||||||
[FromRoute] Guid providerId,
|
[FromRoute] Guid providerId,
|
||||||
[FromRoute] Guid providerOrganizationId,
|
[FromRoute] Guid providerOrganizationId,
|
||||||
@ -113,7 +121,7 @@ public class ProviderClientsController(
|
|||||||
clientOrganization.PlanType,
|
clientOrganization.PlanType,
|
||||||
seatAdjustment);
|
seatAdjustment);
|
||||||
|
|
||||||
if (seatAdjustmentResultsInPurchase && !currentContext.ProviderProviderAdmin(provider.Id))
|
if (seatAdjustmentResultsInPurchase && !_currentContext.ProviderProviderAdmin(provider.Id))
|
||||||
{
|
{
|
||||||
return Error.Unauthorized("Service users cannot purchase additional seats.");
|
return Error.Unauthorized("Service users cannot purchase additional seats.");
|
||||||
}
|
}
|
||||||
@ -127,4 +135,58 @@ public class ProviderClientsController(
|
|||||||
|
|
||||||
return TypedResults.Ok();
|
return TypedResults.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("addable")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task<IResult> GetAddableOrganizationsAsync([FromRoute] Guid providerId)
|
||||||
|
{
|
||||||
|
if (!featureService.IsEnabled(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal))
|
||||||
|
{
|
||||||
|
return Error.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId);
|
||||||
|
|
||||||
|
if (provider == null)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = _currentContext.UserId;
|
||||||
|
|
||||||
|
if (!userId.HasValue)
|
||||||
|
{
|
||||||
|
return Error.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var addable =
|
||||||
|
await providerBillingService.GetAddableOrganizations(provider, userId.Value);
|
||||||
|
|
||||||
|
return TypedResults.Ok(addable);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("existing")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task<IResult> AddExistingOrganizationAsync(
|
||||||
|
[FromRoute] Guid providerId,
|
||||||
|
[FromBody] AddExistingOrganizationRequestBody requestBody)
|
||||||
|
{
|
||||||
|
var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId);
|
||||||
|
|
||||||
|
if (provider == null)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(requestBody.OrganizationId);
|
||||||
|
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
return Error.BadRequest("The organization being added to the provider does not exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await providerBillingService.AddExistingOrganization(provider, organization, requestBody.Key);
|
||||||
|
|
||||||
|
return TypedResults.Ok();
|
||||||
|
}
|
||||||
}
|
}
|
@ -36,7 +36,7 @@ public class EventsController : Controller
|
|||||||
/// If no filters are provided, it will return the last 30 days of event for the organization.
|
/// If no filters are provided, it will return the last 30 days of event for the organization.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType(typeof(ListResponseModel<EventResponseModel>), (int)HttpStatusCode.OK)]
|
[ProducesResponseType(typeof(PagedListResponseModel<EventResponseModel>), (int)HttpStatusCode.OK)]
|
||||||
public async Task<IActionResult> List([FromQuery] EventFilterRequestModel request)
|
public async Task<IActionResult> List([FromQuery] EventFilterRequestModel request)
|
||||||
{
|
{
|
||||||
var dateRange = request.ToDateRange();
|
var dateRange = request.ToDateRange();
|
||||||
@ -65,7 +65,7 @@ public class EventsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var eventResponses = result.Data.Select(e => new EventResponseModel(e));
|
var eventResponses = result.Data.Select(e => new EventResponseModel(e));
|
||||||
var response = new ListResponseModel<EventResponseModel>(eventResponses, result.ContinuationToken);
|
var response = new PagedListResponseModel<EventResponseModel>(eventResponses, result.ContinuationToken);
|
||||||
return new JsonResult(response);
|
return new JsonResult(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Bit.Api.Billing.Models.Requests;
|
||||||
|
|
||||||
|
public class AddExistingOrganizationRequestBody
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "'key' must be provided")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "'organizationId' must be provided")]
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
}
|
@ -4,10 +4,9 @@ namespace Bit.Api.Models.Public.Response;
|
|||||||
|
|
||||||
public class ListResponseModel<T> : IResponseModel where T : IResponseModel
|
public class ListResponseModel<T> : IResponseModel where T : IResponseModel
|
||||||
{
|
{
|
||||||
public ListResponseModel(IEnumerable<T> data, string continuationToken = null)
|
public ListResponseModel(IEnumerable<T> data)
|
||||||
{
|
{
|
||||||
Data = data;
|
Data = data;
|
||||||
ContinuationToken = continuationToken;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -21,8 +20,4 @@ public class ListResponseModel<T> : IResponseModel where T : IResponseModel
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
public IEnumerable<T> Data { get; set; }
|
public IEnumerable<T> Data { get; set; }
|
||||||
/// <summary>
|
|
||||||
/// A cursor for use in pagination.
|
|
||||||
/// </summary>
|
|
||||||
public string ContinuationToken { get; set; }
|
|
||||||
}
|
}
|
||||||
|
10
src/Api/Models/Public/Response/PagedListResponseModel.cs
Normal file
10
src/Api/Models/Public/Response/PagedListResponseModel.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace Bit.Api.Models.Public.Response;
|
||||||
|
|
||||||
|
public class PagedListResponseModel<T>(IEnumerable<T> data, string continuationToken) : ListResponseModel<T>(data)
|
||||||
|
where T : IResponseModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A cursor for use in pagination.
|
||||||
|
/// </summary>
|
||||||
|
public string ContinuationToken { get; set; } = continuationToken;
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
@ -22,4 +23,5 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
|
|||||||
/// Gets the organizations that have a verified domain matching the user's email domain.
|
/// Gets the organizations that have a verified domain matching the user's email domain.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);
|
Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);
|
||||||
|
Task<ICollection<Organization>> GetAddableToProviderByUserIdAsync(Guid userId, ProviderType providerType);
|
||||||
}
|
}
|
||||||
|
8
src/Core/AdminConsole/Services/IEventMessageHandler.cs
Normal file
8
src/Core/AdminConsole/Services/IEventMessageHandler.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public interface IEventMessageHandler
|
||||||
|
{
|
||||||
|
Task HandleEventAsync(EventMessage eventMessage);
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public class EventRepositoryHandler(
|
||||||
|
[FromKeyedServices("persistent")] IEventWriteService eventWriteService)
|
||||||
|
: IEventMessageHandler
|
||||||
|
{
|
||||||
|
public Task HandleEventAsync(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
return eventWriteService.CreateAsync(eventMessage);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public class HttpPostEventHandler : IEventMessageHandler
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly string _httpPostUrl;
|
||||||
|
|
||||||
|
public const string HttpClientName = "HttpPostEventHandlerHttpClient";
|
||||||
|
|
||||||
|
public HttpPostEventHandler(
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
_httpClient = httpClientFactory.CreateClient(HttpClientName);
|
||||||
|
_httpPostUrl = globalSettings.EventLogging.RabbitMq.HttpPostUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleEventAsync(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var content = JsonContent.Create(eventMessage);
|
||||||
|
var response = await _httpClient.PostAsync(_httpPostUrl, content);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using RabbitMQ.Client;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
public class RabbitMqEventWriteService : IEventWriteService, IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly ConnectionFactory _factory;
|
||||||
|
private readonly Lazy<Task<IConnection>> _lazyConnection;
|
||||||
|
private readonly string _exchangeName;
|
||||||
|
|
||||||
|
public RabbitMqEventWriteService(GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
_factory = new ConnectionFactory
|
||||||
|
{
|
||||||
|
HostName = globalSettings.EventLogging.RabbitMq.HostName,
|
||||||
|
UserName = globalSettings.EventLogging.RabbitMq.Username,
|
||||||
|
Password = globalSettings.EventLogging.RabbitMq.Password
|
||||||
|
};
|
||||||
|
_exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName;
|
||||||
|
|
||||||
|
_lazyConnection = new Lazy<Task<IConnection>>(CreateConnectionAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateAsync(IEvent e)
|
||||||
|
{
|
||||||
|
var connection = await _lazyConnection.Value;
|
||||||
|
using var channel = await connection.CreateChannelAsync();
|
||||||
|
|
||||||
|
await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true);
|
||||||
|
|
||||||
|
var body = JsonSerializer.SerializeToUtf8Bytes(e);
|
||||||
|
|
||||||
|
await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: string.Empty, body: body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateManyAsync(IEnumerable<IEvent> events)
|
||||||
|
{
|
||||||
|
var connection = await _lazyConnection.Value;
|
||||||
|
using var channel = await connection.CreateChannelAsync();
|
||||||
|
await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true);
|
||||||
|
|
||||||
|
foreach (var e in events)
|
||||||
|
{
|
||||||
|
var body = JsonSerializer.SerializeToUtf8Bytes(e);
|
||||||
|
|
||||||
|
await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: string.Empty, body: body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_lazyConnection.IsValueCreated)
|
||||||
|
{
|
||||||
|
var connection = await _lazyConnection.Value;
|
||||||
|
await connection.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IConnection> CreateConnectionAsync()
|
||||||
|
{
|
||||||
|
return await _factory.CreateConnectionAsync();
|
||||||
|
}
|
||||||
|
}
|
30
src/Core/Billing/Constants/PlanConstants.cs
Normal file
30
src/Core/Billing/Constants/PlanConstants.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Constants;
|
||||||
|
|
||||||
|
public static class PlanConstants
|
||||||
|
{
|
||||||
|
public static List<PlanType> EnterprisePlanTypes =>
|
||||||
|
[
|
||||||
|
PlanType.EnterpriseAnnually2019,
|
||||||
|
PlanType.EnterpriseAnnually2020,
|
||||||
|
PlanType.EnterpriseAnnually2023,
|
||||||
|
PlanType.EnterpriseAnnually,
|
||||||
|
PlanType.EnterpriseMonthly2019,
|
||||||
|
PlanType.EnterpriseMonthly2020,
|
||||||
|
PlanType.EnterpriseMonthly2023,
|
||||||
|
PlanType.EnterpriseMonthly
|
||||||
|
];
|
||||||
|
|
||||||
|
public static List<PlanType> TeamsPlanTypes =>
|
||||||
|
[
|
||||||
|
PlanType.TeamsAnnually2019,
|
||||||
|
PlanType.TeamsAnnually2020,
|
||||||
|
PlanType.TeamsAnnually2023,
|
||||||
|
PlanType.TeamsAnnually,
|
||||||
|
PlanType.TeamsMonthly2019,
|
||||||
|
PlanType.TeamsMonthly2020,
|
||||||
|
PlanType.TeamsMonthly2023,
|
||||||
|
PlanType.TeamsMonthly
|
||||||
|
];
|
||||||
|
}
|
@ -31,6 +31,16 @@ public static class StripeConstants
|
|||||||
public const string TaxIdInvalid = "tax_id_invalid";
|
public const string TaxIdInvalid = "tax_id_invalid";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class InvoiceStatus
|
||||||
|
{
|
||||||
|
public const string Draft = "draft";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MetadataKeys
|
||||||
|
{
|
||||||
|
public const string OrganizationId = "organizationId";
|
||||||
|
}
|
||||||
|
|
||||||
public static class PaymentBehavior
|
public static class PaymentBehavior
|
||||||
{
|
{
|
||||||
public const string DefaultIncomplete = "default_incomplete";
|
public const string DefaultIncomplete = "default_incomplete";
|
||||||
|
8
src/Core/Billing/Models/AddableOrganization.cs
Normal file
8
src/Core/Billing/Models/AddableOrganization.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace Bit.Core.Billing.Models;
|
||||||
|
|
||||||
|
public record AddableOrganization(
|
||||||
|
Guid Id,
|
||||||
|
string Name,
|
||||||
|
string Plan,
|
||||||
|
int Seats,
|
||||||
|
bool Disabled = false);
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Billing.Models.Sales;
|
using Bit.Core.Billing.Models;
|
||||||
|
using Bit.Core.Billing.Models.Sales;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Services;
|
namespace Bit.Core.Billing.Services;
|
||||||
@ -27,4 +28,9 @@ public interface IPremiumUserBillingService
|
|||||||
/// </code>
|
/// </code>
|
||||||
/// </example>
|
/// </example>
|
||||||
Task Finalize(PremiumUserSale sale);
|
Task Finalize(PremiumUserSale sale);
|
||||||
|
|
||||||
|
Task UpdatePaymentMethod(
|
||||||
|
User user,
|
||||||
|
TokenizedPaymentSource tokenizedPaymentSource,
|
||||||
|
TaxInformation taxInformation);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.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.Models;
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
@ -10,6 +11,11 @@ namespace Bit.Core.Billing.Services;
|
|||||||
|
|
||||||
public interface IProviderBillingService
|
public interface IProviderBillingService
|
||||||
{
|
{
|
||||||
|
Task AddExistingOrganization(
|
||||||
|
Provider provider,
|
||||||
|
Organization organization,
|
||||||
|
string key);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Changes the assigned provider plan for the provider.
|
/// Changes the assigned provider plan for the provider.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -35,6 +41,10 @@ public interface IProviderBillingService
|
|||||||
Task<byte[]> GenerateClientInvoiceReport(
|
Task<byte[]> GenerateClientInvoiceReport(
|
||||||
string invoiceId);
|
string invoiceId);
|
||||||
|
|
||||||
|
Task<IEnumerable<AddableOrganization>> GetAddableOrganizations(
|
||||||
|
Provider provider,
|
||||||
|
Guid userId);
|
||||||
|
|
||||||
/// <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
|
||||||
|
@ -356,11 +356,20 @@ public class OrganizationBillingService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var customerHasTaxInfo = customer is
|
||||||
|
{
|
||||||
|
Address:
|
||||||
|
{
|
||||||
|
Country: not null and not "",
|
||||||
|
PostalCode: not null and not ""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
||||||
{
|
{
|
||||||
AutomaticTax = new SubscriptionAutomaticTaxOptions
|
AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||||
{
|
{
|
||||||
Enabled = true
|
Enabled = customerHasTaxInfo
|
||||||
},
|
},
|
||||||
CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically,
|
CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically,
|
||||||
Customer = customer.Id,
|
Customer = customer.Id,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using Bit.Core.Billing.Caches;
|
using Bit.Core.Billing.Caches;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Models.Sales;
|
using Bit.Core.Billing.Models.Sales;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -58,6 +59,28 @@ public class PremiumUserBillingService(
|
|||||||
await userRepository.ReplaceAsync(user);
|
await userRepository.ReplaceAsync(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task UpdatePaymentMethod(
|
||||||
|
User user,
|
||||||
|
TokenizedPaymentSource tokenizedPaymentSource,
|
||||||
|
TaxInformation taxInformation)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(user.GatewayCustomerId))
|
||||||
|
{
|
||||||
|
var customer = await CreateCustomerAsync(user,
|
||||||
|
new CustomerSetup { TokenizedPaymentSource = tokenizedPaymentSource, TaxInformation = taxInformation });
|
||||||
|
|
||||||
|
user.Gateway = GatewayType.Stripe;
|
||||||
|
user.GatewayCustomerId = customer.Id;
|
||||||
|
|
||||||
|
await userRepository.ReplaceAsync(user);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await subscriberService.UpdatePaymentSource(user, tokenizedPaymentSource);
|
||||||
|
await subscriberService.UpdateTaxInformation(user, taxInformation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<Customer> CreateCustomerAsync(
|
private async Task<Customer> CreateCustomerAsync(
|
||||||
User user,
|
User user,
|
||||||
CustomerSetup customerSetup)
|
CustomerSetup customerSetup)
|
||||||
|
@ -107,10 +107,17 @@ public static class FeatureFlagKeys
|
|||||||
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
||||||
public const string IntegrationPage = "pm-14505-admin-console-integration-page";
|
public const string IntegrationPage = "pm-14505-admin-console-integration-page";
|
||||||
public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications";
|
public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications";
|
||||||
|
public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission";
|
||||||
|
|
||||||
|
/* Tools Team */
|
||||||
|
public const string ItemShare = "item-share";
|
||||||
|
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
||||||
|
public const string MemberAccessReport = "ac-2059-member-access-report";
|
||||||
|
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
|
||||||
|
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
|
||||||
|
|
||||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||||
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
||||||
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 AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section";
|
public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section";
|
||||||
@ -120,7 +127,6 @@ public static class FeatureFlagKeys
|
|||||||
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";
|
||||||
public const string VaultBulkManagementAction = "vault-bulk-management-action";
|
public const string VaultBulkManagementAction = "vault-bulk-management-action";
|
||||||
public const string MemberAccessReport = "ac-2059-member-access-report";
|
|
||||||
public const string InlineMenuFieldQualification = "inline-menu-field-qualification";
|
public const string InlineMenuFieldQualification = "inline-menu-field-qualification";
|
||||||
public const string TwoFactorComponentRefactor = "two-factor-component-refactor";
|
public const string TwoFactorComponentRefactor = "two-factor-component-refactor";
|
||||||
public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements";
|
public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements";
|
||||||
@ -145,9 +151,7 @@ public static class FeatureFlagKeys
|
|||||||
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 PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises";
|
public const string PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises";
|
||||||
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
|
||||||
public const string NewDeviceVerification = "new-device-verification";
|
public const string NewDeviceVerification = "new-device-verification";
|
||||||
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
|
|
||||||
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";
|
||||||
@ -169,7 +173,8 @@ public static class FeatureFlagKeys
|
|||||||
public const string AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner";
|
public const string AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner";
|
||||||
public const string SingleTapPasskeyCreation = "single-tap-passkey-creation";
|
public const string SingleTapPasskeyCreation = "single-tap-passkey-creation";
|
||||||
public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication";
|
public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication";
|
||||||
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
|
public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync";
|
||||||
|
public const string P15179_AddExistingOrgsFromProviderPortal = "PM-15179-add-existing-orgs-from-provider-portal";
|
||||||
|
|
||||||
public static List<string> GetAllKeys()
|
public static List<string> GetAllKeys()
|
||||||
{
|
{
|
||||||
|
@ -21,8 +21,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
||||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.18" />
|
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.28" />
|
||||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.75" />
|
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.85" />
|
||||||
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
||||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
||||||
<PackageReference Include="Google.Protobuf" Version="3.29.2" />
|
<PackageReference Include="Google.Protobuf" Version="3.29.2" />
|
||||||
@ -70,6 +70,7 @@
|
|||||||
<PackageReference Include="Quartz" Version="3.13.1" />
|
<PackageReference Include="Quartz" Version="3.13.1" />
|
||||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.13.1" />
|
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.13.1" />
|
||||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.13.1" />
|
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.13.1" />
|
||||||
|
<PackageReference Include="RabbitMQ.Client" Version="7.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
13
src/Core/Services/EventLoggingListenerService.cs
Normal file
13
src/Core/Services/EventLoggingListenerService.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public abstract class EventLoggingListenerService : BackgroundService
|
||||||
|
{
|
||||||
|
protected readonly IEventMessageHandler _handler;
|
||||||
|
|
||||||
|
protected EventLoggingListenerService(IEventMessageHandler handler)
|
||||||
|
{
|
||||||
|
_handler = handler ?? throw new ArgumentNullException(nameof(handler));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using RabbitMQ.Client;
|
||||||
|
using RabbitMQ.Client.Events;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public class RabbitMqEventListenerService : EventLoggingListenerService
|
||||||
|
{
|
||||||
|
private IChannel _channel;
|
||||||
|
private IConnection _connection;
|
||||||
|
private readonly string _exchangeName;
|
||||||
|
private readonly ConnectionFactory _factory;
|
||||||
|
private readonly ILogger<RabbitMqEventListenerService> _logger;
|
||||||
|
private readonly string _queueName;
|
||||||
|
|
||||||
|
public RabbitMqEventListenerService(
|
||||||
|
IEventMessageHandler handler,
|
||||||
|
ILogger<RabbitMqEventListenerService> logger,
|
||||||
|
GlobalSettings globalSettings,
|
||||||
|
string queueName) : base(handler)
|
||||||
|
{
|
||||||
|
_factory = new ConnectionFactory
|
||||||
|
{
|
||||||
|
HostName = globalSettings.EventLogging.RabbitMq.HostName,
|
||||||
|
UserName = globalSettings.EventLogging.RabbitMq.Username,
|
||||||
|
Password = globalSettings.EventLogging.RabbitMq.Password
|
||||||
|
};
|
||||||
|
_exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName;
|
||||||
|
_logger = logger;
|
||||||
|
_queueName = queueName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_connection = await _factory.CreateConnectionAsync(cancellationToken);
|
||||||
|
_channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
await _channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true);
|
||||||
|
await _channel.QueueDeclareAsync(queue: _queueName,
|
||||||
|
durable: true,
|
||||||
|
exclusive: false,
|
||||||
|
autoDelete: false,
|
||||||
|
arguments: null,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
await _channel.QueueBindAsync(queue: _queueName,
|
||||||
|
exchange: _exchangeName,
|
||||||
|
routingKey: string.Empty,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
await base.StartAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
var consumer = new AsyncEventingBasicConsumer(_channel);
|
||||||
|
consumer.ReceivedAsync += async (_, eventArgs) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var eventMessage = JsonSerializer.Deserialize<EventMessage>(eventArgs.Body.Span);
|
||||||
|
await _handler.HandleEventAsync(eventMessage);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "An error occurred while processing the message");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _channel.BasicConsumeAsync(_queueName, autoAck: true, consumer: consumer, cancellationToken: stoppingToken);
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await Task.Delay(1_000, stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _channel.CloseAsync();
|
||||||
|
await _connection.CloseAsync();
|
||||||
|
await base.StopAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
_channel.Dispose();
|
||||||
|
_connection.Dispose();
|
||||||
|
base.Dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ using Bit.Core.AdminConsole.Services;
|
|||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
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.Billing.Models;
|
||||||
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;
|
||||||
@ -1044,11 +1045,11 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
throw new BadRequestException("Invalid token.");
|
throw new BadRequestException("Invalid token.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var updated = await _paymentService.UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken, taxInfo: taxInfo);
|
var tokenizedPaymentSource = new TokenizedPaymentSource(paymentMethodType, paymentToken);
|
||||||
if (updated)
|
var taxInformation = TaxInformation.From(taxInfo);
|
||||||
{
|
|
||||||
await SaveUserAsync(user);
|
await _premiumUserBillingService.UpdatePaymentMethod(user, tokenizedPaymentSource, taxInformation);
|
||||||
}
|
await SaveUserAsync(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CancelPremiumAsync(User user, bool? endOfPeriod = null)
|
public async Task CancelPremiumAsync(User user, bool? endOfPeriod = null)
|
||||||
|
@ -53,6 +53,7 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
public virtual SqlSettings PostgreSql { get; set; } = new SqlSettings();
|
public virtual SqlSettings PostgreSql { get; set; } = new SqlSettings();
|
||||||
public virtual SqlSettings MySql { get; set; } = new SqlSettings();
|
public virtual SqlSettings MySql { get; set; } = new SqlSettings();
|
||||||
public virtual SqlSettings Sqlite { get; set; } = new SqlSettings() { ConnectionString = "Data Source=:memory:" };
|
public virtual SqlSettings Sqlite { get; set; } = new SqlSettings() { ConnectionString = "Data Source=:memory:" };
|
||||||
|
public virtual EventLoggingSettings EventLogging { get; set; } = new EventLoggingSettings();
|
||||||
public virtual MailSettings Mail { get; set; } = new MailSettings();
|
public virtual MailSettings Mail { get; set; } = new MailSettings();
|
||||||
public virtual IConnectionStringSettings Storage { get; set; } = new ConnectionStringSettings();
|
public virtual IConnectionStringSettings Storage { get; set; } = new ConnectionStringSettings();
|
||||||
public virtual ConnectionStringSettings Events { get; set; } = new ConnectionStringSettings();
|
public virtual ConnectionStringSettings Events { get; set; } = new ConnectionStringSettings();
|
||||||
@ -256,6 +257,44 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class EventLoggingSettings
|
||||||
|
{
|
||||||
|
public RabbitMqSettings RabbitMq { get; set; } = new RabbitMqSettings();
|
||||||
|
|
||||||
|
public class RabbitMqSettings
|
||||||
|
{
|
||||||
|
private string _hostName;
|
||||||
|
private string _username;
|
||||||
|
private string _password;
|
||||||
|
private string _exchangeName;
|
||||||
|
|
||||||
|
public virtual string EventRepositoryQueueName { get; set; } = "events-write-queue";
|
||||||
|
public virtual string HttpPostQueueName { get; set; } = "events-httpPost-queue";
|
||||||
|
public virtual string HttpPostUrl { get; set; }
|
||||||
|
|
||||||
|
public string HostName
|
||||||
|
{
|
||||||
|
get => _hostName;
|
||||||
|
set => _hostName = value.Trim('"');
|
||||||
|
}
|
||||||
|
public string Username
|
||||||
|
{
|
||||||
|
get => _username;
|
||||||
|
set => _username = value.Trim('"');
|
||||||
|
}
|
||||||
|
public string Password
|
||||||
|
{
|
||||||
|
get => _password;
|
||||||
|
set => _password = value.Trim('"');
|
||||||
|
}
|
||||||
|
public string ExchangeName
|
||||||
|
{
|
||||||
|
get => _exchangeName;
|
||||||
|
set => _exchangeName = value.Trim('"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class ConnectionStringSettings : IConnectionStringSettings
|
public class ConnectionStringSettings : IConnectionStringSettings
|
||||||
{
|
{
|
||||||
private string _connectionString;
|
private string _connectionString;
|
||||||
|
@ -27,4 +27,5 @@ public interface IGlobalSettings
|
|||||||
string DatabaseProvider { get; set; }
|
string DatabaseProvider { get; set; }
|
||||||
GlobalSettings.SqlSettings SqlServer { get; set; }
|
GlobalSettings.SqlSettings SqlServer { get; set; }
|
||||||
string DevelopmentDirectory { get; set; }
|
string DevelopmentDirectory { get; set; }
|
||||||
|
GlobalSettings.EventLoggingSettings EventLogging { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -82,6 +82,35 @@ public class Startup
|
|||||||
{
|
{
|
||||||
services.AddHostedService<Core.HostedServices.ApplicationCacheHostedService>();
|
services.AddHostedService<Core.HostedServices.ApplicationCacheHostedService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional RabbitMQ Listeners
|
||||||
|
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) &&
|
||||||
|
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) &&
|
||||||
|
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) &&
|
||||||
|
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName))
|
||||||
|
{
|
||||||
|
services.AddSingleton<EventRepositoryHandler>();
|
||||||
|
services.AddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("persistent");
|
||||||
|
services.AddSingleton<IHostedService>(provider =>
|
||||||
|
new RabbitMqEventListenerService(
|
||||||
|
provider.GetRequiredService<EventRepositoryHandler>(),
|
||||||
|
provider.GetRequiredService<ILogger<RabbitMqEventListenerService>>(),
|
||||||
|
provider.GetRequiredService<GlobalSettings>(),
|
||||||
|
globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName));
|
||||||
|
|
||||||
|
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HttpPostUrl))
|
||||||
|
{
|
||||||
|
services.AddSingleton<HttpPostEventHandler>();
|
||||||
|
services.AddHttpClient(HttpPostEventHandler.HttpClientName);
|
||||||
|
|
||||||
|
services.AddSingleton<IHostedService>(provider =>
|
||||||
|
new RabbitMqEventListenerService(
|
||||||
|
provider.GetRequiredService<HttpPostEventHandler>(),
|
||||||
|
provider.GetRequiredService<ILogger<RabbitMqEventListenerService>>(),
|
||||||
|
provider.GetRequiredService<GlobalSettings>(),
|
||||||
|
globalSettings.EventLogging.RabbitMq.HttpPostQueueName));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Configure(
|
public void Configure(
|
||||||
|
@ -4,6 +4,7 @@ using Bit.Core.Context;
|
|||||||
using Bit.Core.Tools.Enums;
|
using Bit.Core.Tools.Enums;
|
||||||
using Bit.Core.Tools.Models.Business;
|
using Bit.Core.Tools.Models.Business;
|
||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tools.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Bit.SharedWeb.Utilities;
|
using Bit.SharedWeb.Utilities;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -17,6 +18,7 @@ public class AccountsController(
|
|||||||
IReferenceEventService referenceEventService) : Microsoft.AspNetCore.Mvc.Controller
|
IReferenceEventService referenceEventService) : Microsoft.AspNetCore.Mvc.Controller
|
||||||
{
|
{
|
||||||
[HttpPost("trial/send-verification-email")]
|
[HttpPost("trial/send-verification-email")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<IActionResult> PostTrialInitiationSendVerificationEmailAsync([FromBody] TrialSendVerificationEmailRequestModel model)
|
public async Task<IActionResult> PostTrialInitiationSendVerificationEmailAsync([FromBody] TrialSendVerificationEmailRequestModel model)
|
||||||
{
|
{
|
||||||
var token = await sendTrialInitiationEmailForRegistrationCommand.Handle(
|
var token = await sendTrialInitiationEmailForRegistrationCommand.Handle(
|
||||||
|
@ -85,28 +85,17 @@ public class DeviceValidator(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point we have established either new device verification is not required or the NewDeviceOtp is valid
|
// At this point we have established either new device verification is not required or the NewDeviceOtp is valid,
|
||||||
|
// so we save the device to the database and proceed with authentication
|
||||||
requestDevice.UserId = context.User.Id;
|
requestDevice.UserId = context.User.Id;
|
||||||
await _deviceService.SaveAsync(requestDevice);
|
await _deviceService.SaveAsync(requestDevice);
|
||||||
context.Device = requestDevice;
|
context.Device = requestDevice;
|
||||||
|
|
||||||
// backwards compatibility -- If NewDeviceVerification not enabled send the new login emails
|
if (!_globalSettings.DisableEmailNewDevice)
|
||||||
// PM-13340: removal Task; remove entire if block emails should no longer be sent
|
|
||||||
if (!_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification))
|
|
||||||
{
|
{
|
||||||
// This ensures the user doesn't receive a "new device" email on the first login
|
await SendNewDeviceLoginEmail(context.User, requestDevice);
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
if (now - context.User.CreationDate > TimeSpan.FromMinutes(10))
|
|
||||||
{
|
|
||||||
var deviceType = requestDevice.Type.GetType().GetMember(requestDevice.Type.ToString())
|
|
||||||
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName();
|
|
||||||
if (!_globalSettings.DisableEmailNewDevice)
|
|
||||||
{
|
|
||||||
await _mailService.SendNewDeviceLoggedInEmail(context.User.Email, deviceType, now,
|
|
||||||
_currentContext.IpAddress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,6 +163,19 @@ public class DeviceValidator(
|
|||||||
return DeviceValidationResultType.NewDeviceVerificationRequired;
|
return DeviceValidationResultType.NewDeviceVerificationRequired;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SendNewDeviceLoginEmail(User user, Device requestDevice)
|
||||||
|
{
|
||||||
|
// Ensure that the user doesn't receive a "new device" email on the first login
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (now - user.CreationDate > TimeSpan.FromMinutes(10))
|
||||||
|
{
|
||||||
|
var deviceType = requestDevice.Type.GetType().GetMember(requestDevice.Type.ToString())
|
||||||
|
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName();
|
||||||
|
await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now,
|
||||||
|
_currentContext.IpAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Device> GetKnownDeviceAsync(User user, Device device)
|
public async Task<Device> GetKnownDeviceAsync(User user, Device device)
|
||||||
{
|
{
|
||||||
if (user == null || device == null)
|
if (user == null || device == null)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
@ -180,4 +181,19 @@ public class OrganizationRepository : Repository<Organization, Guid>, IOrganizat
|
|||||||
return result.ToList();
|
return result.ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ICollection<Organization>> GetAddableToProviderByUserIdAsync(
|
||||||
|
Guid userId,
|
||||||
|
ProviderType providerType)
|
||||||
|
{
|
||||||
|
using (var connection = new SqlConnection(ConnectionString))
|
||||||
|
{
|
||||||
|
var result = await connection.QueryAsync<Organization>(
|
||||||
|
$"[{Schema}].[{Table}_ReadAddableToProviderByUserId]",
|
||||||
|
new { UserId = userId, ProviderType = providerType },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
return result.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using AutoMapper.QueryableExtensions;
|
using AutoMapper.QueryableExtensions;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
using LinqToDB.Tools;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -114,13 +117,19 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
|||||||
|
|
||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
|
||||||
|
var disallowedPlanTypes = new List<PlanType>
|
||||||
|
{
|
||||||
|
PlanType.Free,
|
||||||
|
PlanType.Custom,
|
||||||
|
PlanType.FamiliesAnnually2019,
|
||||||
|
PlanType.FamiliesAnnually
|
||||||
|
};
|
||||||
|
|
||||||
var query =
|
var query =
|
||||||
from o in dbContext.Organizations
|
from o in dbContext.Organizations
|
||||||
where
|
where o.PlanType.NotIn(disallowedPlanTypes) &&
|
||||||
((o.PlanType >= PlanType.TeamsMonthly2019 && o.PlanType <= PlanType.EnterpriseAnnually2019) ||
|
!dbContext.ProviderOrganizations.Any(po => po.OrganizationId == o.Id) &&
|
||||||
(o.PlanType >= PlanType.TeamsMonthly2020 && o.PlanType <= PlanType.EnterpriseAnnually)) &&
|
(string.IsNullOrWhiteSpace(name) || EF.Functions.Like(o.Name, $"%{name}%"))
|
||||||
!dbContext.ProviderOrganizations.Any(po => po.OrganizationId == o.Id) &&
|
|
||||||
(string.IsNullOrWhiteSpace(name) || EF.Functions.Like(o.Name, $"%{name}%"))
|
|
||||||
select o;
|
select o;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(ownerEmail))
|
if (string.IsNullOrWhiteSpace(ownerEmail))
|
||||||
@ -152,7 +161,7 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
|||||||
select o;
|
select o;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await query.OrderByDescending(o => o.CreationDate).Skip(skip).Take(take).ToArrayAsync();
|
return await query.OrderByDescending(o => o.CreationDate).ThenByDescending(o => o.Id).Skip(skip).Take(take).ToArrayAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateStorageAsync(Guid id)
|
public async Task UpdateStorageAsync(Guid id)
|
||||||
@ -298,6 +307,41 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ICollection<Core.AdminConsole.Entities.Organization>> GetAddableToProviderByUserIdAsync(
|
||||||
|
Guid userId,
|
||||||
|
ProviderType providerType)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
|
||||||
|
var planTypes = providerType switch
|
||||||
|
{
|
||||||
|
ProviderType.Msp => PlanConstants.EnterprisePlanTypes.Concat(PlanConstants.TeamsPlanTypes),
|
||||||
|
ProviderType.MultiOrganizationEnterprise => PlanConstants.EnterprisePlanTypes,
|
||||||
|
_ => []
|
||||||
|
};
|
||||||
|
|
||||||
|
var query =
|
||||||
|
from organizationUser in dbContext.OrganizationUsers
|
||||||
|
join organization in dbContext.Organizations on organizationUser.OrganizationId equals organization.Id
|
||||||
|
where
|
||||||
|
organizationUser.UserId == userId &&
|
||||||
|
organizationUser.Type == OrganizationUserType.Owner &&
|
||||||
|
organizationUser.Status == OrganizationUserStatusType.Confirmed &&
|
||||||
|
organization.Enabled &&
|
||||||
|
organization.GatewayCustomerId != null &&
|
||||||
|
organization.GatewaySubscriptionId != null &&
|
||||||
|
organization.Seats > 0 &&
|
||||||
|
organization.Status == OrganizationStatusType.Created &&
|
||||||
|
!organization.UseSecretsManager &&
|
||||||
|
organization.PlanType.In(planTypes)
|
||||||
|
select organization;
|
||||||
|
|
||||||
|
return await query.ToArrayAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Task EnableCollectionEnhancements(Guid organizationId)
|
public Task EnableCollectionEnhancements(Guid organizationId)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException("Collection enhancements migration is not yet supported for Entity Framework.");
|
throw new NotImplementedException("Collection enhancements migration is not yet supported for Entity Framework.");
|
||||||
|
@ -46,27 +46,17 @@ public class OrganizationDomainRepository : Repository<Core.Entities.Organizatio
|
|||||||
using var scope = ServiceScopeFactory.CreateScope();
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
|
||||||
var domains = await dbContext.OrganizationDomains
|
var start36HoursWindow = date.AddHours(-36);
|
||||||
.Where(x => x.VerifiedDate == null
|
var end36HoursWindow = date;
|
||||||
&& x.JobRunCount != 3
|
|
||||||
&& x.NextRunDate.Year == date.Year
|
var pastDomains = await dbContext.OrganizationDomains
|
||||||
&& x.NextRunDate.Month == date.Month
|
.Where(x => x.NextRunDate >= start36HoursWindow
|
||||||
&& x.NextRunDate.Day == date.Day
|
&& x.NextRunDate <= end36HoursWindow
|
||||||
&& x.NextRunDate.Hour == date.Hour)
|
&& x.VerifiedDate == null
|
||||||
.AsNoTracking()
|
&& x.JobRunCount != 3)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
//Get records that have ignored/failed by the background service
|
return Mapper.Map<List<Core.Entities.OrganizationDomain>>(pastDomains);
|
||||||
var pastDomains = dbContext.OrganizationDomains
|
|
||||||
.AsEnumerable()
|
|
||||||
.Where(x => (date - x.NextRunDate).TotalHours > 36
|
|
||||||
&& x.VerifiedDate == null
|
|
||||||
&& x.JobRunCount != 3)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var results = domains.Union(pastDomains);
|
|
||||||
|
|
||||||
return Mapper.Map<List<Core.Entities.OrganizationDomain>>(results);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OrganizationDomainSsoDetailsData?> GetOrganizationDomainSsoDetailsAsync(string email)
|
public async Task<OrganizationDomainSsoDetailsData?> GetOrganizationDomainSsoDetailsAsync(string email)
|
||||||
|
@ -325,7 +325,17 @@ public static class ServiceCollectionExtensions
|
|||||||
}
|
}
|
||||||
else if (globalSettings.SelfHosted)
|
else if (globalSettings.SelfHosted)
|
||||||
{
|
{
|
||||||
services.AddSingleton<IEventWriteService, RepositoryEventWriteService>();
|
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) &&
|
||||||
|
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) &&
|
||||||
|
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) &&
|
||||||
|
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName))
|
||||||
|
{
|
||||||
|
services.AddSingleton<IEventWriteService, RabbitMqEventWriteService>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
services.AddSingleton<IEventWriteService, RepositoryEventWriteService>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[Organization_ReadAddableToProviderByUserId]
|
||||||
|
@UserId UNIQUEIDENTIFIER,
|
||||||
|
@ProviderType TINYINT
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
SELECT O.* FROM [dbo].[OrganizationUser] AS OU
|
||||||
|
JOIN [dbo].[Organization] AS O ON O.[Id] = OU.[OrganizationId]
|
||||||
|
WHERE
|
||||||
|
OU.[UserId] = @UserId AND
|
||||||
|
OU.[Type] = 0 AND
|
||||||
|
OU.[Status] = 2 AND
|
||||||
|
O.[Enabled] = 1 AND
|
||||||
|
O.[GatewayCustomerId] IS NOT NULL AND
|
||||||
|
O.[GatewaySubscriptionId] IS NOT NULL AND
|
||||||
|
O.[Seats] > 0 AND
|
||||||
|
O.[Status] = 1 AND
|
||||||
|
O.[UseSecretsManager] = 0 AND
|
||||||
|
-- All Teams & Enterprise for MSP
|
||||||
|
(@ProviderType = 0 AND O.[PlanType] IN (2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20) OR
|
||||||
|
-- All Enterprise for MOE
|
||||||
|
@ProviderType = 2 AND O.[PlanType] IN (4, 5, 10, 11, 14, 15, 19, 20));
|
||||||
|
END
|
@ -1,5 +1,5 @@
|
|||||||
CREATE PROCEDURE [dbo].[Organization_UnassignedToProviderSearch]
|
CREATE PROCEDURE [dbo].[Organization_UnassignedToProviderSearch]
|
||||||
@Name NVARCHAR(50),
|
@Name NVARCHAR(55),
|
||||||
@OwnerEmail NVARCHAR(256),
|
@OwnerEmail NVARCHAR(256),
|
||||||
@Skip INT = 0,
|
@Skip INT = 0,
|
||||||
@Take INT = 25
|
@Take INT = 25
|
||||||
@ -21,11 +21,11 @@ BEGIN
|
|||||||
INNER JOIN
|
INNER JOIN
|
||||||
[dbo].[User] U ON U.[Id] = OU.[UserId]
|
[dbo].[User] U ON U.[Id] = OU.[UserId]
|
||||||
WHERE
|
WHERE
|
||||||
((O.[PlanType] >= 2 AND O.[PlanType] <= 5) OR (O.[PlanType] >= 8 AND O.[PlanType] <= 20) AND (O.PlanType <> 16)) -- All 'Teams' and 'Enterprise' organizations
|
O.[PlanType] NOT IN (0, 1, 6, 7) -- Not 'Free', 'Custom' or 'Families'
|
||||||
AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id])
|
AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id])
|
||||||
AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)
|
AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)
|
||||||
AND (U.[Email] LIKE @OwnerLikeSearch)
|
AND (U.[Email] LIKE @OwnerLikeSearch)
|
||||||
ORDER BY O.[CreationDate] DESC
|
ORDER BY O.[CreationDate] DESC, O.[Id]
|
||||||
OFFSET @Skip ROWS
|
OFFSET @Skip ROWS
|
||||||
FETCH NEXT @Take ROWS ONLY
|
FETCH NEXT @Take ROWS ONLY
|
||||||
END
|
END
|
||||||
@ -36,10 +36,10 @@ BEGIN
|
|||||||
FROM
|
FROM
|
||||||
[dbo].[OrganizationView] O
|
[dbo].[OrganizationView] O
|
||||||
WHERE
|
WHERE
|
||||||
((O.[PlanType] >= 2 AND O.[PlanType] <= 5) OR (O.[PlanType] >= 8 AND O.[PlanType] <= 20) AND (O.PlanType <> 16)) -- All 'Teams' and 'Enterprise' organizations
|
O.[PlanType] NOT IN (0, 1, 6, 7) -- Not 'Free', 'Custom' or 'Families'
|
||||||
AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id])
|
AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id])
|
||||||
AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)
|
AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)
|
||||||
ORDER BY O.[CreationDate] DESC
|
ORDER BY O.[CreationDate] DESC, O.[Id]
|
||||||
OFFSET @Skip ROWS
|
OFFSET @Skip ROWS
|
||||||
FETCH NEXT @Take ROWS ONLY
|
FETCH NEXT @Take ROWS ONLY
|
||||||
END
|
END
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Bit.Api.Billing.Controllers;
|
using Bit.Api.AdminConsole.Controllers;
|
||||||
using Bit.Api.Billing.Models.Requests;
|
using Bit.Api.Billing.Models.Requests;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
@ -19,10 +19,9 @@ using Microsoft.AspNetCore.Http.HttpResults;
|
|||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using NSubstitute.ReturnsExtensions;
|
using NSubstitute.ReturnsExtensions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
using static Bit.Api.Test.Billing.Utilities;
|
using static Bit.Api.Test.Billing.Utilities;
|
||||||
|
|
||||||
namespace Bit.Api.Test.Billing.Controllers;
|
namespace Bit.Api.Test.AdminConsole.Controllers;
|
||||||
|
|
||||||
[ControllerCustomize(typeof(ProviderClientsController))]
|
[ControllerCustomize(typeof(ProviderClientsController))]
|
||||||
[SutProviderCustomize]
|
[SutProviderCustomize]
|
@ -8,6 +8,8 @@ public class MockedHttpMessageHandler : HttpMessageHandler
|
|||||||
{
|
{
|
||||||
private readonly List<IHttpRequestMatcher> _matchers = new();
|
private readonly List<IHttpRequestMatcher> _matchers = new();
|
||||||
|
|
||||||
|
public List<HttpRequestMessage> CapturedRequests { get; } = new List<HttpRequestMessage>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The fallback handler to use when the request does not match any of the provided matchers.
|
/// The fallback handler to use when the request does not match any of the provided matchers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -16,6 +18,7 @@ public class MockedHttpMessageHandler : HttpMessageHandler
|
|||||||
|
|
||||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
CapturedRequests.Add(request);
|
||||||
var matcher = _matchers.FirstOrDefault(x => x.Matches(request));
|
var matcher = _matchers.FirstOrDefault(x => x.Matches(request));
|
||||||
if (matcher == null)
|
if (matcher == null)
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Bit.Test.Common.Helpers;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Services;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class EventRepositoryHandlerTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task HandleEventAsync_WritesEventToIEventWriteService(
|
||||||
|
EventMessage eventMessage,
|
||||||
|
SutProvider<EventRepositoryHandler> sutProvider)
|
||||||
|
{
|
||||||
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateAsync(
|
||||||
|
Arg.Is(AssertHelper.AssertPropertyEqual<IEvent>(eventMessage))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Bit.Test.Common.Helpers;
|
||||||
|
using Bit.Test.Common.MockedHttpClient;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Services;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class HttpPostEventHandlerTests
|
||||||
|
{
|
||||||
|
private readonly MockedHttpMessageHandler _handler;
|
||||||
|
private HttpClient _httpClient;
|
||||||
|
|
||||||
|
private const string _httpPostUrl = "http://localhost/test/event";
|
||||||
|
|
||||||
|
public HttpPostEventHandlerTests()
|
||||||
|
{
|
||||||
|
_handler = new MockedHttpMessageHandler();
|
||||||
|
_handler.Fallback
|
||||||
|
.WithStatusCode(HttpStatusCode.OK)
|
||||||
|
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||||
|
_httpClient = _handler.ToHttpClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SutProvider<HttpPostEventHandler> GetSutProvider()
|
||||||
|
{
|
||||||
|
var clientFactory = Substitute.For<IHttpClientFactory>();
|
||||||
|
clientFactory.CreateClient(HttpPostEventHandler.HttpClientName).Returns(_httpClient);
|
||||||
|
|
||||||
|
var globalSettings = new GlobalSettings();
|
||||||
|
globalSettings.EventLogging.RabbitMq.HttpPostUrl = _httpPostUrl;
|
||||||
|
|
||||||
|
return new SutProvider<HttpPostEventHandler>()
|
||||||
|
.SetDependency(globalSettings)
|
||||||
|
.SetDependency(clientFactory)
|
||||||
|
.Create();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task HandleEventAsync_PostsEventsToUrl(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var sutProvider = GetSutProvider();
|
||||||
|
var content = JsonContent.Create(eventMessage);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||||
|
Arg.Is(AssertHelper.AssertPropertyEqual<string>(HttpPostEventHandler.HttpClientName))
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.Single(_handler.CapturedRequests);
|
||||||
|
var request = _handler.CapturedRequests[0];
|
||||||
|
Assert.NotNull(request);
|
||||||
|
var returned = await request.Content.ReadFromJsonAsync<EventMessage>();
|
||||||
|
|
||||||
|
Assert.Equal(HttpMethod.Post, request.Method);
|
||||||
|
Assert.Equal(_httpPostUrl, request.RequestUri.ToString());
|
||||||
|
AssertHelper.AssertPropertyEqual(eventMessage, returned, new[] { "IdempotencyId" });
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Events.Models;
|
||||||
|
|
||||||
|
namespace Bit.Events.IntegrationTest.Controllers;
|
||||||
|
|
||||||
|
public class CollectControllerTests
|
||||||
|
{
|
||||||
|
// This is a very simple test, and should be updated to assert more things, but for now
|
||||||
|
// it ensures that the events startup doesn't throw any errors with fairly basic configuration.
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_Works()
|
||||||
|
{
|
||||||
|
var eventsApplicationFactory = new EventsApplicationFactory();
|
||||||
|
var (accessToken, _) = await eventsApplicationFactory.LoginWithNewAccount();
|
||||||
|
var client = eventsApplicationFactory.CreateAuthedClient(accessToken);
|
||||||
|
|
||||||
|
var response = await client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.User_ClientExportedVault,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
}
|
29
test/Events.IntegrationTest/Events.IntegrationTest.csproj
Normal file
29
test/Events.IntegrationTest/Events.IntegrationTest.csproj
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||||
|
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="$(CoverletCollectorVersion)">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\Events\Events.csproj" />
|
||||||
|
<ProjectReference Include="..\IntegrationTestCommon\IntegrationTestCommon.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
57
test/Events.IntegrationTest/EventsApplicationFactory.cs
Normal file
57
test/Events.IntegrationTest/EventsApplicationFactory.cs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
using Bit.Identity.Models.Request.Accounts;
|
||||||
|
using Bit.IntegrationTestCommon.Factories;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.TestHost;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.Events.IntegrationTest;
|
||||||
|
|
||||||
|
public class EventsApplicationFactory : WebApplicationFactoryBase<Startup>
|
||||||
|
{
|
||||||
|
private readonly IdentityApplicationFactory _identityApplicationFactory;
|
||||||
|
private const string _connectionString = "DataSource=:memory:";
|
||||||
|
|
||||||
|
public EventsApplicationFactory()
|
||||||
|
{
|
||||||
|
SqliteConnection = new SqliteConnection(_connectionString);
|
||||||
|
SqliteConnection.Open();
|
||||||
|
|
||||||
|
_identityApplicationFactory = new IdentityApplicationFactory();
|
||||||
|
_identityApplicationFactory.SqliteConnection = SqliteConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||||
|
{
|
||||||
|
base.ConfigureWebHost(builder);
|
||||||
|
|
||||||
|
builder.ConfigureTestServices(services =>
|
||||||
|
{
|
||||||
|
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
|
||||||
|
{
|
||||||
|
options.BackchannelHttpHandler = _identityApplicationFactory.Server.CreateHandler();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper for registering and logging in to a new account
|
||||||
|
/// </summary>
|
||||||
|
public async Task<(string Token, string RefreshToken)> LoginWithNewAccount(string email = "integration-test@bitwarden.com", string masterPasswordHash = "master_password_hash")
|
||||||
|
{
|
||||||
|
await _identityApplicationFactory.RegisterAsync(new RegisterRequestModel
|
||||||
|
{
|
||||||
|
Email = email,
|
||||||
|
MasterPasswordHash = masterPasswordHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
SqliteConnection!.Dispose();
|
||||||
|
}
|
||||||
|
}
|
1
test/Events.IntegrationTest/GlobalUsings.cs
Normal file
1
test/Events.IntegrationTest/GlobalUsings.cs
Normal file
@ -0,0 +1 @@
|
|||||||
|
global using Xunit;
|
@ -227,7 +227,7 @@ public class DeviceValidatorTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_SendsEmail_ReturnsTrue(
|
public async void ValidateRequestDeviceAsync_ExistingUserNewDeviceLogin_SendNewDeviceLoginEmail_ReturnsTrue(
|
||||||
CustomValidatorRequestContext context,
|
CustomValidatorRequestContext context,
|
||||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||||
{
|
{
|
||||||
@ -237,8 +237,6 @@ public class DeviceValidatorTests
|
|||||||
_globalSettings.DisableEmailNewDevice = false;
|
_globalSettings.DisableEmailNewDevice = false;
|
||||||
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
|
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
|
||||||
.Returns(null as Device);
|
.Returns(null as Device);
|
||||||
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
|
|
||||||
.Returns(false);
|
|
||||||
// set user creation to more than 10 minutes ago
|
// set user creation to more than 10 minutes ago
|
||||||
context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11);
|
context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11);
|
||||||
|
|
||||||
@ -253,7 +251,7 @@ public class DeviceValidatorTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_NewUser_DoesNotSendEmail_ReturnsTrue(
|
public async void ValidateRequestDeviceAsync_NewUserNewDeviceLogin_DoesNotSendNewDeviceLoginEmail_ReturnsTrue(
|
||||||
CustomValidatorRequestContext context,
|
CustomValidatorRequestContext context,
|
||||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||||
{
|
{
|
||||||
@ -263,8 +261,6 @@ public class DeviceValidatorTests
|
|||||||
_globalSettings.DisableEmailNewDevice = false;
|
_globalSettings.DisableEmailNewDevice = false;
|
||||||
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
|
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
|
||||||
.Returns(null as Device);
|
.Returns(null as Device);
|
||||||
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
|
|
||||||
.Returns(false);
|
|
||||||
// set user creation to less than 10 minutes ago
|
// set user creation to less than 10 minutes ago
|
||||||
context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(9);
|
context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(9);
|
||||||
|
|
||||||
@ -279,7 +275,7 @@ public class DeviceValidatorTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_DisableEmailTrue_DoesNotSendEmail_ReturnsTrue(
|
public async void ValidateRequestDeviceAsynce_DisableNewDeviceLoginEmailTrue_DoesNotSendNewDeviceEmail_ReturnsTrue(
|
||||||
CustomValidatorRequestContext context,
|
CustomValidatorRequestContext context,
|
||||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||||
{
|
{
|
||||||
@ -289,8 +285,6 @@ public class DeviceValidatorTests
|
|||||||
_globalSettings.DisableEmailNewDevice = true;
|
_globalSettings.DisableEmailNewDevice = true;
|
||||||
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
|
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
|
||||||
.Returns(null as Device);
|
.Returns(null as Device);
|
||||||
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
|
|
||||||
.Returns(false);
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="xunit" Version="2.9.2" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
@ -188,4 +188,122 @@ public class OrganizationDomainRepositoryTests
|
|||||||
var expectedDomain2 = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain2.DomainName);
|
var expectedDomain2 = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain2.DomainName);
|
||||||
Assert.Null(expectedDomain2);
|
Assert.Null(expectedDomain2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task GetManyByNextRunDateAsync_ShouldReturnUnverifiedDomains(
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationDomainRepository organizationDomainRepository)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
|
||||||
|
var organization1 = await organizationRepository.CreateAsync(new Organization
|
||||||
|
{
|
||||||
|
Name = $"Test Org {id}",
|
||||||
|
BillingEmail = $"test+{id}@example.com",
|
||||||
|
Plan = "Test",
|
||||||
|
PrivateKey = "privatekey",
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var organizationDomain = new OrganizationDomain
|
||||||
|
{
|
||||||
|
OrganizationId = organization1.Id,
|
||||||
|
DomainName = $"domain2+{id}@example.com",
|
||||||
|
Txt = "btw+12345"
|
||||||
|
};
|
||||||
|
|
||||||
|
var within36HoursWindow = 1;
|
||||||
|
organizationDomain.SetNextRunDate(within36HoursWindow);
|
||||||
|
|
||||||
|
await organizationDomainRepository.CreateAsync(organizationDomain);
|
||||||
|
|
||||||
|
var date = organizationDomain.NextRunDate;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var domains = await organizationDomainRepository.GetManyByNextRunDateAsync(date);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain.DomainName);
|
||||||
|
Assert.NotNull(expectedDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task GetManyByNextRunDateAsync_ShouldNotReturnUnverifiedDomains_WhenNextRunDateIsOutside36hoursWindow(
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationDomainRepository organizationDomainRepository)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
|
||||||
|
var organization1 = await organizationRepository.CreateAsync(new Organization
|
||||||
|
{
|
||||||
|
Name = $"Test Org {id}",
|
||||||
|
BillingEmail = $"test+{id}@example.com",
|
||||||
|
Plan = "Test",
|
||||||
|
PrivateKey = "privatekey",
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var organizationDomain = new OrganizationDomain
|
||||||
|
{
|
||||||
|
OrganizationId = organization1.Id,
|
||||||
|
DomainName = $"domain2+{id}@example.com",
|
||||||
|
Txt = "btw+12345"
|
||||||
|
};
|
||||||
|
|
||||||
|
var outside36HoursWindow = 20;
|
||||||
|
organizationDomain.SetNextRunDate(outside36HoursWindow);
|
||||||
|
|
||||||
|
await organizationDomainRepository.CreateAsync(organizationDomain);
|
||||||
|
|
||||||
|
var date = DateTimeOffset.UtcNow.Date.AddDays(1);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var domains = await organizationDomainRepository.GetManyByNextRunDateAsync(date);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain.DomainName);
|
||||||
|
Assert.Null(expectedDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task GetManyByNextRunDateAsync_ShouldNotReturnVerifiedDomains(
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationDomainRepository organizationDomainRepository)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
|
||||||
|
var organization1 = await organizationRepository.CreateAsync(new Organization
|
||||||
|
{
|
||||||
|
Name = $"Test Org {id}",
|
||||||
|
BillingEmail = $"test+{id}@example.com",
|
||||||
|
Plan = "Test",
|
||||||
|
PrivateKey = "privatekey",
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var organizationDomain = new OrganizationDomain
|
||||||
|
{
|
||||||
|
OrganizationId = organization1.Id,
|
||||||
|
DomainName = $"domain2+{id}@example.com",
|
||||||
|
Txt = "btw+12345"
|
||||||
|
};
|
||||||
|
|
||||||
|
var within36HoursWindow = 1;
|
||||||
|
organizationDomain.SetNextRunDate(within36HoursWindow);
|
||||||
|
organizationDomain.SetVerifiedDate();
|
||||||
|
|
||||||
|
await organizationDomainRepository.CreateAsync(organizationDomain);
|
||||||
|
|
||||||
|
var date = DateTimeOffset.UtcNow.Date.AddDays(1);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var domains = await organizationDomainRepository.GetManyByNextRunDateAsync(date);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain.DomainName);
|
||||||
|
Assert.Null(expectedDomain);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ using Microsoft.Data.Sqlite;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
@ -188,44 +189,27 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
|||||||
// QUESTION: The normal licensing service should run fine on developer machines but not in CI
|
// QUESTION: The normal licensing service should run fine on developer machines but not in CI
|
||||||
// should we have a fork here to leave the normal service for developers?
|
// should we have a fork here to leave the normal service for developers?
|
||||||
// TODO: Eventually add the license file to CI
|
// TODO: Eventually add the license file to CI
|
||||||
var licensingService = services.First(sd => sd.ServiceType == typeof(ILicensingService));
|
Replace<ILicensingService, NoopLicensingService>(services);
|
||||||
services.Remove(licensingService);
|
|
||||||
services.AddSingleton<ILicensingService, NoopLicensingService>();
|
|
||||||
|
|
||||||
// FUTURE CONSIDERATION: Add way to run this self hosted/cloud, for now it is cloud only
|
// FUTURE CONSIDERATION: Add way to run this self hosted/cloud, for now it is cloud only
|
||||||
var pushRegistrationService = services.First(sd => sd.ServiceType == typeof(IPushRegistrationService));
|
Replace<IPushRegistrationService, NoopPushRegistrationService>(services);
|
||||||
services.Remove(pushRegistrationService);
|
|
||||||
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
|
|
||||||
|
|
||||||
// Even though we are cloud we currently set this up as cloud, we can use the EF/selfhosted service
|
// Even though we are cloud we currently set this up as cloud, we can use the EF/selfhosted service
|
||||||
// instead of using Noop for this service
|
// instead of using Noop for this service
|
||||||
// TODO: Install and use azurite in CI pipeline
|
// TODO: Install and use azurite in CI pipeline
|
||||||
var eventWriteService = services.First(sd => sd.ServiceType == typeof(IEventWriteService));
|
Replace<IEventWriteService, RepositoryEventWriteService>(services);
|
||||||
services.Remove(eventWriteService);
|
|
||||||
services.AddSingleton<IEventWriteService, RepositoryEventWriteService>();
|
|
||||||
|
|
||||||
var eventRepositoryService = services.First(sd => sd.ServiceType == typeof(IEventRepository));
|
Replace<IEventRepository, EventRepository>(services);
|
||||||
services.Remove(eventRepositoryService);
|
|
||||||
services.AddSingleton<IEventRepository, EventRepository>();
|
|
||||||
|
|
||||||
var mailDeliveryService = services.First(sd => sd.ServiceType == typeof(IMailDeliveryService));
|
Replace<IMailDeliveryService, NoopMailDeliveryService>(services);
|
||||||
services.Remove(mailDeliveryService);
|
|
||||||
services.AddSingleton<IMailDeliveryService, NoopMailDeliveryService>();
|
|
||||||
|
|
||||||
var captchaValidationService = services.First(sd => sd.ServiceType == typeof(ICaptchaValidationService));
|
Replace<ICaptchaValidationService, NoopCaptchaValidationService>(services);
|
||||||
services.Remove(captchaValidationService);
|
|
||||||
services.AddSingleton<ICaptchaValidationService, NoopCaptchaValidationService>();
|
|
||||||
|
|
||||||
// TODO: Install and use azurite in CI pipeline
|
// TODO: Install and use azurite in CI pipeline
|
||||||
var installationDeviceRepository =
|
Replace<IInstallationDeviceRepository, NoopRepos.InstallationDeviceRepository>(services);
|
||||||
services.First(sd => sd.ServiceType == typeof(IInstallationDeviceRepository));
|
|
||||||
services.Remove(installationDeviceRepository);
|
|
||||||
services.AddSingleton<IInstallationDeviceRepository, NoopRepos.InstallationDeviceRepository>();
|
|
||||||
|
|
||||||
// TODO: Install and use azurite in CI pipeline
|
// TODO: Install and use azurite in CI pipeline
|
||||||
var referenceEventService = services.First(sd => sd.ServiceType == typeof(IReferenceEventService));
|
Replace<IReferenceEventService, NoopReferenceEventService>(services);
|
||||||
services.Remove(referenceEventService);
|
|
||||||
services.AddSingleton<IReferenceEventService, NoopReferenceEventService>();
|
|
||||||
|
|
||||||
// Our Rate limiter works so well that it begins to fail tests unless we carve out
|
// Our Rate limiter works so well that it begins to fail tests unless we carve out
|
||||||
// one whitelisted ip. We should still test the rate limiter though and they should change the Ip
|
// one whitelisted ip. We should still test the rate limiter though and they should change the Ip
|
||||||
@ -245,14 +229,9 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
|||||||
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
|
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
|
||||||
|
|
||||||
// Noop StripePaymentService - this could be changed to integrate with our Stripe test account
|
// Noop StripePaymentService - this could be changed to integrate with our Stripe test account
|
||||||
var stripePaymentService = services.First(sd => sd.ServiceType == typeof(IPaymentService));
|
Replace(services, Substitute.For<IPaymentService>());
|
||||||
services.Remove(stripePaymentService);
|
|
||||||
services.AddSingleton(Substitute.For<IPaymentService>());
|
|
||||||
|
|
||||||
var organizationBillingService =
|
Replace(services, Substitute.For<IOrganizationBillingService>());
|
||||||
services.First(sd => sd.ServiceType == typeof(IOrganizationBillingService));
|
|
||||||
services.Remove(organizationBillingService);
|
|
||||||
services.AddSingleton(Substitute.For<IOrganizationBillingService>());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
foreach (var configureTestService in _configureTestServices)
|
foreach (var configureTestService in _configureTestServices)
|
||||||
@ -261,6 +240,35 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void Replace<TService, TNewImplementation>(IServiceCollection services)
|
||||||
|
where TService : class
|
||||||
|
where TNewImplementation : class, TService
|
||||||
|
{
|
||||||
|
services.RemoveAll<TService>();
|
||||||
|
services.AddSingleton<TService, TNewImplementation>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Replace<TService>(IServiceCollection services, TService implementation)
|
||||||
|
where TService : class
|
||||||
|
{
|
||||||
|
services.RemoveAll<TService>();
|
||||||
|
services.AddSingleton<TService>(implementation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClient CreateAuthedClient(string accessToken)
|
||||||
|
{
|
||||||
|
var handler = Server.CreateHandler((context) =>
|
||||||
|
{
|
||||||
|
context.Request.Headers.Authorization = $"Bearer {accessToken}";
|
||||||
|
});
|
||||||
|
|
||||||
|
return new HttpClient(handler)
|
||||||
|
{
|
||||||
|
BaseAddress = Server.BaseAddress,
|
||||||
|
Timeout = TimeSpan.FromSeconds(200),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public DatabaseContext GetDatabaseContext()
|
public DatabaseContext GetDatabaseContext()
|
||||||
{
|
{
|
||||||
var scope = Services.CreateScope();
|
var scope = Services.CreateScope();
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
-- Drop existing SPROC
|
||||||
|
IF OBJECT_ID('[dbo].[Organization_ReadAddableToProviderByUserId') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP PROCEDURE [dbo].[Organization_ReadAddableToProviderByUserId]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE PROCEDURE [dbo].[Organization_ReadAddableToProviderByUserId]
|
||||||
|
@UserId UNIQUEIDENTIFIER,
|
||||||
|
@ProviderType TINYINT
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
SELECT O.* FROM [dbo].[OrganizationUser] AS OU
|
||||||
|
JOIN [dbo].[Organization] AS O ON O.[Id] = OU.[OrganizationId]
|
||||||
|
WHERE
|
||||||
|
OU.[UserId] = @UserId AND
|
||||||
|
OU.[Type] = 0 AND
|
||||||
|
OU.[Status] = 2 AND
|
||||||
|
O.[Enabled] = 1 AND
|
||||||
|
O.[GatewayCustomerId] IS NOT NULL AND
|
||||||
|
O.[GatewaySubscriptionId] IS NOT NULL AND
|
||||||
|
O.[Seats] > 0 AND
|
||||||
|
O.[Status] = 1 AND
|
||||||
|
O.[UseSecretsManager] = 0 AND
|
||||||
|
-- All Teams & Enterprise for MSP
|
||||||
|
(@ProviderType = 0 AND O.[PlanType] IN (2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20) OR
|
||||||
|
-- All Enterprise for MOE
|
||||||
|
@ProviderType = 2 AND O.[PlanType] IN (4, 5, 10, 11, 14, 15, 19, 20));
|
||||||
|
END
|
||||||
|
GO
|
@ -0,0 +1,54 @@
|
|||||||
|
-- Drop existing SPROC
|
||||||
|
IF OBJECT_ID('[dbo].[Organization_UnassignedToProviderSearch]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP PROCEDURE [dbo].[Organization_UnassignedToProviderSearch]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE PROCEDURE [dbo].[Organization_UnassignedToProviderSearch]
|
||||||
|
@Name NVARCHAR(55),
|
||||||
|
@OwnerEmail NVARCHAR(256),
|
||||||
|
@Skip INT = 0,
|
||||||
|
@Take INT = 25
|
||||||
|
WITH RECOMPILE
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
DECLARE @NameLikeSearch NVARCHAR(55) = '%' + @Name + '%'
|
||||||
|
DECLARE @OwnerLikeSearch NVARCHAR(55) = @OwnerEmail + '%'
|
||||||
|
|
||||||
|
IF @OwnerEmail IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
SELECT
|
||||||
|
O.*
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationView] O
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[OrganizationUser] OU ON O.[Id] = OU.[OrganizationId]
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[User] U ON U.[Id] = OU.[UserId]
|
||||||
|
WHERE
|
||||||
|
O.[PlanType] NOT IN (0, 1, 6, 7) -- Not 'Free', 'Custom' or 'Families'
|
||||||
|
AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id])
|
||||||
|
AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)
|
||||||
|
AND (U.[Email] LIKE @OwnerLikeSearch)
|
||||||
|
ORDER BY O.[CreationDate] DESC, O.[Id]
|
||||||
|
OFFSET @Skip ROWS
|
||||||
|
FETCH NEXT @Take ROWS ONLY
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
BEGIN
|
||||||
|
SELECT
|
||||||
|
O.*
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationView] O
|
||||||
|
WHERE
|
||||||
|
O.[PlanType] NOT IN (0, 1, 6, 7) -- Not 'Free', 'Custom' or 'Families'
|
||||||
|
AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id])
|
||||||
|
AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)
|
||||||
|
ORDER BY O.[CreationDate] DESC, O.[Id]
|
||||||
|
OFFSET @Skip ROWS
|
||||||
|
FETCH NEXT @Take ROWS ONLY
|
||||||
|
END
|
||||||
|
END
|
||||||
|
GO
|
@ -0,0 +1,7 @@
|
|||||||
|
-- Refresh Views
|
||||||
|
|
||||||
|
IF OBJECT_ID('[dbo].[OrganizationView]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
EXECUTE sp_refreshview N'[dbo].[OrganizationView]';
|
||||||
|
END
|
||||||
|
GO
|
Loading…
x
Reference in New Issue
Block a user