1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-17 07:30:59 -05:00

Merge branch 'main' into ac/pm-18238/add-requiretwofactorpolicyrequirement

This commit is contained in:
Rui Tome
2025-05-22 10:07:27 +01:00
85 changed files with 582 additions and 259 deletions

View File

@ -0,0 +1,187 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
public record ProviderClientOrganizationSignUpResponse(
Organization Organization,
Collection DefaultCollection);
public interface IProviderClientOrganizationSignUpCommand
{
/// <summary>
/// Sign up a new client organization for a provider.
/// </summary>
/// <param name="signup">The signup information.</param>
/// <returns>A tuple containing the new organization and its default collection.</returns>
Task<ProviderClientOrganizationSignUpResponse> SignUpClientOrganizationAsync(OrganizationSignup signup);
}
public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizationSignUpCommand
{
public const string PlanNullErrorMessage = "Password Manager Plan was null.";
public const string PlanDisabledErrorMessage = "Password Manager Plan is disabled.";
public const string AdditionalSeatsNegativeErrorMessage = "You can't subtract Password Manager seats!";
private readonly ICurrentContext _currentContext;
private readonly IPricingClient _pricingClient;
private readonly IReferenceEventService _referenceEventService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
private readonly IApplicationCacheService _applicationCacheService;
private readonly ICollectionRepository _collectionRepository;
public ProviderClientOrganizationSignUpCommand(
ICurrentContext currentContext,
IPricingClient pricingClient,
IReferenceEventService referenceEventService,
IOrganizationRepository organizationRepository,
IOrganizationApiKeyRepository organizationApiKeyRepository,
IApplicationCacheService applicationCacheService,
ICollectionRepository collectionRepository)
{
_currentContext = currentContext;
_pricingClient = pricingClient;
_referenceEventService = referenceEventService;
_organizationRepository = organizationRepository;
_organizationApiKeyRepository = organizationApiKeyRepository;
_applicationCacheService = applicationCacheService;
_collectionRepository = collectionRepository;
}
public async Task<ProviderClientOrganizationSignUpResponse> SignUpClientOrganizationAsync(OrganizationSignup signup)
{
var plan = await _pricingClient.GetPlanOrThrow(signup.Plan);
ValidatePlan(plan, signup.AdditionalSeats);
var organization = new Organization
{
// Pre-generate the org id so that we can save it with the Stripe subscription.
Id = CoreHelpers.GenerateComb(),
Name = signup.Name,
BillingEmail = signup.BillingEmail,
PlanType = plan!.Type,
Seats = signup.AdditionalSeats,
MaxCollections = plan.PasswordManager.MaxCollections,
MaxStorageGb = 1,
UsePolicies = plan.HasPolicies,
UseSso = plan.HasSso,
UseOrganizationDomains = plan.HasOrganizationDomains,
UseGroups = plan.HasGroups,
UseEvents = plan.HasEvents,
UseDirectory = plan.HasDirectory,
UseTotp = plan.HasTotp,
Use2fa = plan.Has2fa,
UseApi = plan.HasApi,
UseResetPassword = plan.HasResetPassword,
SelfHost = plan.HasSelfHost,
UsersGetPremium = plan.UsersGetPremium,
UseCustomPermissions = plan.HasCustomPermissions,
UseScim = plan.HasScim,
Plan = plan.Name,
Gateway = GatewayType.Stripe,
ReferenceData = signup.Owner.ReferenceData,
Enabled = true,
LicenseKey = CoreHelpers.SecureRandomString(20),
PublicKey = signup.PublicKey,
PrivateKey = signup.PrivateKey,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Status = OrganizationStatusType.Created,
UsePasswordManager = true,
// Secrets Manager not available for purchase with Consolidated Billing.
UseSecretsManager = false,
};
var returnValue = await SignUpAsync(organization, signup.CollectionName);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext)
{
PlanName = plan.Name,
PlanType = plan.Type,
Seats = returnValue.Organization.Seats,
SignupInitiationPath = signup.InitiationPath,
Storage = returnValue.Organization.MaxStorageGb,
});
return returnValue;
}
private static void ValidatePlan(Plan plan, int additionalSeats)
{
if (plan is null)
{
throw new BadRequestException(PlanNullErrorMessage);
}
if (plan.Disabled)
{
throw new BadRequestException(PlanDisabledErrorMessage);
}
if (additionalSeats < 0)
{
throw new BadRequestException(AdditionalSeatsNegativeErrorMessage);
}
}
/// <summary>
/// Private helper method to create a new organization.
/// </summary>
private async Task<ProviderClientOrganizationSignUpResponse> SignUpAsync(
Organization organization, string collectionName)
{
try
{
await _organizationRepository.CreateAsync(organization);
await _organizationApiKeyRepository.CreateAsync(new OrganizationApiKey
{
OrganizationId = organization.Id,
ApiKey = CoreHelpers.SecureRandomString(30),
Type = OrganizationApiKeyType.Default,
RevisionDate = DateTime.UtcNow,
});
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
Collection defaultCollection = null;
if (!string.IsNullOrWhiteSpace(collectionName))
{
defaultCollection = new Collection
{
Name = collectionName,
OrganizationId = organization.Id,
CreationDate = organization.CreationDate,
RevisionDate = organization.CreationDate
};
await _collectionRepository.CreateAsync(defaultCollection, null, null);
}
return new ProviderClientOrganizationSignUpResponse(organization, defaultCollection);
}
catch
{
if (organization.Id != default)
{
await _organizationRepository.DeleteAsync(organization);
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
}
throw;
}
}
}

View File

@ -18,9 +18,6 @@ public interface IOrganizationService
Task AutoAddSeatsAsync(Organization organization, int seatsToAdd);
Task<string> AdjustSeatsAsync(Guid organizationId, int seatAdjustment);
Task VerifyBankAsync(Guid organizationId, int amount1, int amount2);
#nullable enable
Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup);
#nullable disable
/// <summary>
/// Create a new organization on a self-hosted instance
/// </summary>

View File

@ -403,66 +403,6 @@ public class OrganizationService : IOrganizationService
}
}
public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup)
{
var plan = await _pricingClient.GetPlanOrThrow(signup.Plan);
ValidatePlan(plan, signup.AdditionalSeats, "Password Manager");
var organization = new Organization
{
// Pre-generate the org id so that we can save it with the Stripe subscription.
Id = CoreHelpers.GenerateComb(),
Name = signup.Name,
BillingEmail = signup.BillingEmail,
PlanType = plan!.Type,
Seats = signup.AdditionalSeats,
MaxCollections = plan.PasswordManager.MaxCollections,
MaxStorageGb = 1,
UsePolicies = plan.HasPolicies,
UseSso = plan.HasSso,
UseOrganizationDomains = plan.HasOrganizationDomains,
UseGroups = plan.HasGroups,
UseEvents = plan.HasEvents,
UseDirectory = plan.HasDirectory,
UseTotp = plan.HasTotp,
Use2fa = plan.Has2fa,
UseApi = plan.HasApi,
UseResetPassword = plan.HasResetPassword,
SelfHost = plan.HasSelfHost,
UsersGetPremium = plan.UsersGetPremium,
UseCustomPermissions = plan.HasCustomPermissions,
UseScim = plan.HasScim,
Plan = plan.Name,
Gateway = GatewayType.Stripe,
ReferenceData = signup.Owner.ReferenceData,
Enabled = true,
LicenseKey = CoreHelpers.SecureRandomString(20),
PublicKey = signup.PublicKey,
PrivateKey = signup.PrivateKey,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Status = OrganizationStatusType.Created,
UsePasswordManager = true,
// Secrets Manager not available for purchase with Consolidated Billing.
UseSecretsManager = false,
};
var returnValue = await SignUpAsync(organization, default, signup.OwnerKey, signup.CollectionName, false);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext)
{
PlanName = plan.Name,
PlanType = plan.Type,
Seats = returnValue.Item1.Seats,
SignupInitiationPath = signup.InitiationPath,
Storage = returnValue.Item1.MaxStorageGb,
});
return returnValue;
}
private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
{
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);

View File

@ -1,12 +1,12 @@
using System.ComponentModel.DataAnnotations;
#nullable enable
using System.ComponentModel.DataAnnotations;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Utilities;
#nullable enable
namespace Bit.Core.Billing.Entities;
namespace Bit.Core.Billing.Providers.Entities;
public class ClientOrganizationMigrationRecord : ITableObject<Guid>
{

View File

@ -1,10 +1,10 @@
using System.ComponentModel.DataAnnotations;
#nullable enable
using System.ComponentModel.DataAnnotations;
using Bit.Core.Entities;
using Bit.Core.Utilities;
#nullable enable
namespace Bit.Core.Billing.Entities;
namespace Bit.Core.Billing.Providers.Entities;
public class ProviderInvoiceItem : ITableObject<Guid>
{

View File

@ -1,10 +1,10 @@
using Bit.Core.Billing.Enums;
#nullable enable
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Utilities;
#nullable enable
namespace Bit.Core.Billing.Entities;
namespace Bit.Core.Billing.Providers.Entities;
public class ProviderPlan : ITableObject<Guid>
{

View File

@ -1,4 +1,4 @@
namespace Bit.Core.Billing.Migration.Models;
namespace Bit.Core.Billing.Providers.Migration.Models;
public enum ClientMigrationProgress
{

View File

@ -1,6 +1,6 @@
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Providers.Entities;
namespace Bit.Core.Billing.Migration.Models;
namespace Bit.Core.Billing.Providers.Migration.Models;
public class ProviderMigrationResult
{

View File

@ -1,4 +1,4 @@
namespace Bit.Core.Billing.Migration.Models;
namespace Bit.Core.Billing.Providers.Migration.Models;
public enum ProviderMigrationProgress
{

View File

@ -1,8 +1,8 @@
using Bit.Core.Billing.Migration.Services;
using Bit.Core.Billing.Migration.Services.Implementations;
using Bit.Core.Billing.Providers.Migration.Services;
using Bit.Core.Billing.Providers.Migration.Services.Implementations;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Billing.Migration;
namespace Bit.Core.Billing.Providers.Migration;
public static class ServiceCollectionExtensions
{

View File

@ -1,8 +1,8 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Billing.Migration.Models;
using Bit.Core.Billing.Providers.Migration.Models;
namespace Bit.Core.Billing.Migration.Services;
namespace Bit.Core.Billing.Providers.Migration.Services;
public interface IMigrationTrackerCache
{

View File

@ -1,6 +1,6 @@
using Bit.Core.AdminConsole.Entities;
namespace Bit.Core.Billing.Migration.Services;
namespace Bit.Core.Billing.Providers.Migration.Services;
public interface IOrganizationMigrator
{

View File

@ -1,6 +1,6 @@
using Bit.Core.Billing.Migration.Models;
using Bit.Core.Billing.Providers.Migration.Models;
namespace Bit.Core.Billing.Migration.Services;
namespace Bit.Core.Billing.Providers.Migration.Services;
public interface IProviderMigrator
{

View File

@ -1,11 +1,11 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Billing.Migration.Models;
using Bit.Core.Billing.Providers.Migration.Models;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Billing.Migration.Services.Implementations;
namespace Bit.Core.Billing.Providers.Migration.Services.Implementations;
public class MigrationTrackerDistributedCache(
[FromKeyedServices("persistent")]

View File

@ -1,10 +1,10 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Migration.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Billing.Providers.Migration.Models;
using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -12,7 +12,7 @@ using Microsoft.Extensions.Logging;
using Stripe;
using Plan = Bit.Core.Models.StaticStore.Plan;
namespace Bit.Core.Billing.Migration.Services.Implementations;
namespace Bit.Core.Billing.Providers.Migration.Services.Implementations;
public class OrganizationMigrator(
IClientOrganizationMigrationRecordRepository clientOrganizationMigrationRecordRepository,

View File

@ -3,18 +3,18 @@ using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Migration.Models;
using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Contracts;
using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Billing.Providers.Migration.Models;
using Bit.Core.Billing.Providers.Models;
using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.Extensions.Logging;
using Stripe;
namespace Bit.Core.Billing.Migration.Services.Implementations;
namespace Bit.Core.Billing.Providers.Migration.Services.Implementations;
public class ProviderMigrator(
IClientOrganizationMigrationRecordRepository clientOrganizationMigrationRecordRepository,

View File

@ -1,4 +1,4 @@
namespace Bit.Core.Billing.Models;
namespace Bit.Core.Billing.Providers.Models;
public record AddableOrganization(
Guid Id,

View File

@ -1,7 +1,7 @@
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Billing.Enums;
namespace Bit.Core.Billing.Services.Contracts;
namespace Bit.Core.Billing.Providers.Models;
public record ChangeProviderPlanCommand(
Provider Provider,

View File

@ -1,11 +1,12 @@
using Bit.Core.Models.StaticStore;
namespace Bit.Core.Billing.Models;
namespace Bit.Core.Billing.Providers.Models;
public record ConfiguredProviderPlan(
Guid Id,
Guid ProviderId,
Plan Plan,
decimal Price,
int SeatMinimum,
int PurchasedSeats,
int AssignedSeats);

View File

@ -1,7 +1,7 @@
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Billing.Enums;
namespace Bit.Core.Billing.Services.Contracts;
namespace Bit.Core.Billing.Providers.Models;
/// <param name="Provider">The provider to update the seat minimums for.</param>
/// <param name="Configuration">The new seat minimums for the provider.</param>

View File

@ -1,7 +1,7 @@
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Repositories;
namespace Bit.Core.Billing.Repositories;
namespace Bit.Core.Billing.Providers.Repositories;
public interface IClientOrganizationMigrationRecordRepository : IRepository<ClientOrganizationMigrationRecord, Guid>
{

View File

@ -1,7 +1,7 @@
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Repositories;
namespace Bit.Core.Billing.Repositories;
namespace Bit.Core.Billing.Providers.Repositories;
public interface IProviderInvoiceItemRepository : IRepository<ProviderInvoiceItem, Guid>
{

View File

@ -1,7 +1,7 @@
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Repositories;
namespace Bit.Core.Billing.Repositories;
namespace Bit.Core.Billing.Providers.Repositories;
public interface IProviderPlanRepository : IRepository<ProviderPlan, Guid>
{

View File

@ -3,7 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using OneOf;
namespace Bit.Core.Billing.Services;
namespace Bit.Core.Billing.Providers.Services;
public interface IBusinessUnitConverter
{

View File

@ -1,14 +1,14 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Services.Contracts;
using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Billing.Providers.Models;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Models.Business;
using Stripe;
namespace Bit.Core.Billing.Services;
namespace Bit.Core.Billing.Providers.Services;
public interface IProviderBillingService
{

View File

@ -2,7 +2,7 @@
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
namespace Bit.Core.Billing.Services.Contracts;
namespace Bit.Core.Billing.Tax.Models;
public class AutomaticTaxFactoryParameters
{

View File

@ -1,4 +1,4 @@
using Bit.Core.Billing.Services.Contracts;
using Bit.Core.Billing.Tax.Models;
namespace Bit.Core.Billing.Tax.Services;

View File

@ -1,7 +1,7 @@
#nullable enable
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services.Contracts;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Entities;
using Bit.Core.Services;

View File

@ -150,6 +150,7 @@ public static class FeatureFlagKeys
public const string UseOrganizationWarningsService = "use-organization-warnings-service";
public const string PM20322_AllowTrialLength0 = "pm-20322-allow-trial-length-0";
public const string PM21092_SetNonUSBusinessUseToReverseCharge = "pm-21092-set-non-us-business-use-to-reverse-charge";
public const string PM21383_GetProviderPriceFromStripe = "pm-21383-get-provider-price-from-stripe";
/* Data Insights and Reporting Team */
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";

View File

@ -69,8 +69,11 @@ public static class OrganizationServiceCollectionExtensions
services.AddBaseOrganizationSubscriptionCommandsQueries();
}
private static IServiceCollection AddOrganizationSignUpCommands(this IServiceCollection services) =>
private static void AddOrganizationSignUpCommands(this IServiceCollection services)
{
services.AddScoped<ICloudOrganizationSignUpCommand, CloudOrganizationSignUpCommand>();
services.AddScoped<IProviderClientOrganizationSignUpCommand, ProviderClientOrganizationSignUpCommand>();
}
private static void AddOrganizationDeleteCommands(this IServiceCollection services)
{

View File

@ -57,4 +57,5 @@ public interface IStripeAdapter
Task<SetupIntent> SetupIntentGet(string id, SetupIntentGetOptions options = null);
Task SetupIntentVerifyMicroDeposit(string id, SetupIntentVerifyMicrodepositsOptions options);
Task<List<Stripe.TestHelpers.TestClock>> TestClockListAsync();
Task<Price> PriceGetAsync(string id, PriceGetOptions options = null);
}

View File

@ -283,4 +283,7 @@ public class StripeAdapter : IStripeAdapter
}
return items;
}
public Task<Price> PriceGetAsync(string id, PriceGetOptions options = null)
=> _priceService.GetAsync(id, options);
}

View File

@ -7,7 +7,7 @@ using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Business;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services.Contracts;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Billing.Tax.Requests;
using Bit.Core.Billing.Tax.Responses;
using Bit.Core.Billing.Tax.Services;