1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 05:00:19 -05:00

[PM-15179] Implement endpoints to add existing organization to CB provider (#5310)

* Implement endpoints to add existing organization to provider

* Run dotnet format

* Support MOE

* Run dotnet format

* Move ProviderClientsController under AC ownership

* Move ProviderClientsControllerTests under AC ownership

* Jared's feedback
This commit is contained in:
Alex Morask 2025-02-04 09:02:18 -05:00 committed by GitHub
parent 90f308db34
commit f1b9bd9a09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 427 additions and 6 deletions

View File

@ -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()
};
}
} }

View File

@ -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();
}
} }

View File

@ -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; }
}

View File

@ -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);
} }

View 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
];
}

View File

@ -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";

View File

@ -0,0 +1,8 @@
namespace Bit.Core.Billing.Models;
public record AddableOrganization(
Guid Id,
string Name,
string Plan,
int Seats,
bool Disabled = false);

View File

@ -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

View File

@ -172,6 +172,7 @@ public static class FeatureFlagKeys
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 EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync"; 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()
{ {

View File

@ -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();
}
}
} }

View File

@ -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;
@ -298,6 +301,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.");

View File

@ -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

View File

@ -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]

View File

@ -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