1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-04 01:22:50 -05:00

Merge branch 'main' into ac/pm-15161/create-resellerclientorganizationsignup-command

This commit is contained in:
Rui Tomé
2025-05-06 10:30:13 +01:00
committed by GitHub
89 changed files with 2972 additions and 266 deletions

View File

@ -0,0 +1,37 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Entities;
using Bit.Core.Enums;
#nullable enable
namespace Bit.Core.Models.Data.Integrations;
public class IntegrationTemplateContext(EventMessage eventMessage)
{
public EventMessage Event { get; } = eventMessage;
public string DomainName => Event.DomainName;
public string IpAddress => Event.IpAddress;
public DeviceType? DeviceType => Event.DeviceType;
public Guid? ActingUserId => Event.ActingUserId;
public Guid? OrganizationUserId => Event.OrganizationUserId;
public DateTime Date => Event.Date;
public EventType Type => Event.Type;
public Guid? UserId => Event.UserId;
public Guid? OrganizationId => Event.OrganizationId;
public Guid? CipherId => Event.CipherId;
public Guid? CollectionId => Event.CollectionId;
public Guid? GroupId => Event.GroupId;
public Guid? PolicyId => Event.PolicyId;
public User? User { get; set; }
public string? UserName => User?.Name;
public string? UserEmail => User?.Email;
public User? ActingUser { get; set; }
public string? ActingUserName => ActingUser?.Name;
public string? ActingUserEmail => ActingUser?.Email;
public Organization? Organization { get; set; }
public string? OrganizationName => Organization?.DisplayName();
}

View File

@ -0,0 +1,128 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tokens;
using Microsoft.AspNetCore.DataProtection;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
{
private readonly IOrganizationService _organizationService;
private readonly ICollectionRepository _collectionRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
private readonly IDataProtector _dataProtector;
private readonly IGlobalSettings _globalSettings;
private readonly IPolicyService _policyService;
private readonly IOrganizationUserRepository _organizationUserRepository;
public InitPendingOrganizationCommand(
IOrganizationService organizationService,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IDataProtectionProvider dataProtectionProvider,
IGlobalSettings globalSettings,
IPolicyService policyService,
IOrganizationUserRepository organizationUserRepository
)
{
_organizationService = organizationService;
_collectionRepository = collectionRepository;
_organizationRepository = organizationRepository;
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
_dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose);
_globalSettings = globalSettings;
_policyService = policyService;
_organizationUserRepository = organizationUserRepository;
}
public async Task InitPendingOrganizationAsync(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken)
{
await ValidateSignUpPoliciesAsync(user.Id);
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
if (orgUser == null)
{
throw new BadRequestException("User invalid.");
}
var tokenValid = ValidateInviteToken(orgUser, user, emailToken);
if (!tokenValid)
{
throw new BadRequestException("Invalid token");
}
var org = await _organizationRepository.GetByIdAsync(organizationId);
if (org.Enabled)
{
throw new BadRequestException("Organization is already enabled.");
}
if (org.Status != OrganizationStatusType.Pending)
{
throw new BadRequestException("Organization is not on a Pending status.");
}
if (!string.IsNullOrEmpty(org.PublicKey))
{
throw new BadRequestException("Organization already has a Public Key.");
}
if (!string.IsNullOrEmpty(org.PrivateKey))
{
throw new BadRequestException("Organization already has a Private Key.");
}
org.Enabled = true;
org.Status = OrganizationStatusType.Created;
org.PublicKey = publicKey;
org.PrivateKey = privateKey;
await _organizationService.UpdateAsync(org);
if (!string.IsNullOrWhiteSpace(collectionName))
{
// give the owner Can Manage access over the default collection
List<CollectionAccessSelection> defaultOwnerAccess =
[new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }];
var defaultCollection = new Collection
{
Name = collectionName,
OrganizationId = org.Id
};
await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess);
}
}
private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
{
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
if (anySingleOrgPolicies)
{
throw new BadRequestException("You may not create an organization. You belong to an organization " +
"which has a policy that prohibits you from being a member of any other organization.");
}
}
private bool ValidateInviteToken(OrganizationUser orgUser, User user, string emailToken)
{
var tokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken(
_orgUserInviteTokenDataFactory, emailToken, orgUser);
return tokenValid;
}
}

View File

@ -0,0 +1,13 @@
using Bit.Core.Entities;
namespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
public interface IInitPendingOrganizationCommand
{
/// <summary>
/// Update an Organization entry by setting the public/private keys, set it as 'Enabled' and move the Status from 'Pending' to 'Created'.
/// </summary>
/// <remarks>
/// This method must target a disabled Organization that has null keys and status as 'Pending'.
/// </remarks>
Task InitPendingOrganizationAsync(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken);
}

View File

@ -45,14 +45,8 @@ public interface IOrganizationService
Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid organizationId,
IEnumerable<Guid> organizationUserIds, Guid? revokingUserId);
Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted);
/// <summary>
/// Update an Organization entry by setting the public/private keys, set it as 'Enabled' and move the Status from 'Pending' to 'Created'.
/// </summary>
/// <remarks>
/// This method must target a disabled Organization that has null keys and status as 'Pending'.
/// </remarks>
Task InitPendingOrganization(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken);
Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null);
Task<(bool canScale, string failureReason)> CanScaleAsync(Organization organization, int seatsToAdd);
void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade);
void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade);

View File

@ -1,5 +1,6 @@
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Models.Business.Provider;
using Bit.Core.Billing.Models;
using Bit.Core.Entities;
using Bit.Core.Models.Business;
@ -7,7 +8,8 @@ namespace Bit.Core.AdminConsole.Services;
public interface IProviderService
{
Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo = null);
Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo,
TokenizedPaymentSource tokenizedPaymentSource = null);
Task UpdateAsync(Provider provider, bool updateBilling = false);
Task<List<ProviderUser>> InviteUserAsync(ProviderUserInvite<string> invite);

View File

@ -0,0 +1,34 @@
using Bit.Core.Models.Data;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Services;
public class EventRouteService(
[FromKeyedServices("broadcast")] IEventWriteService broadcastEventWriteService,
[FromKeyedServices("storage")] IEventWriteService storageEventWriteService,
IFeatureService _featureService) : IEventWriteService
{
public async Task CreateAsync(IEvent e)
{
if (_featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations))
{
await broadcastEventWriteService.CreateAsync(e);
}
else
{
await storageEventWriteService.CreateAsync(e);
}
}
public async Task CreateManyAsync(IEnumerable<IEvent> e)
{
if (_featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations))
{
await broadcastEventWriteService.CreateManyAsync(e);
}
else
{
await storageEventWriteService.CreateManyAsync(e);
}
}
}

View File

@ -0,0 +1,66 @@
using System.Text.Json.Nodes;
using Bit.Core.AdminConsole.Utilities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Integrations;
using Bit.Core.Repositories;
namespace Bit.Core.Services;
public abstract class IntegrationEventHandlerBase(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationIntegrationConfigurationRepository configurationRepository)
: IEventMessageHandler
{
public async Task HandleEventAsync(EventMessage eventMessage)
{
var organizationId = eventMessage.OrganizationId ?? Guid.Empty;
var configurations = await configurationRepository.GetConfigurationDetailsAsync(
organizationId,
GetIntegrationType(),
eventMessage.Type);
foreach (var configuration in configurations)
{
var context = await BuildContextAsync(eventMessage, configuration.Template);
var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(configuration.Template, context);
await ProcessEventIntegrationAsync(configuration.MergedConfiguration, renderedTemplate);
}
}
public async Task HandleManyEventsAsync(IEnumerable<EventMessage> eventMessages)
{
foreach (var eventMessage in eventMessages)
{
await HandleEventAsync(eventMessage);
}
}
private async Task<IntegrationTemplateContext> BuildContextAsync(EventMessage eventMessage, string template)
{
var context = new IntegrationTemplateContext(eventMessage);
if (IntegrationTemplateProcessor.TemplateRequiresUser(template) && eventMessage.UserId.HasValue)
{
context.User = await userRepository.GetByIdAsync(eventMessage.UserId.Value);
}
if (IntegrationTemplateProcessor.TemplateRequiresActingUser(template) && eventMessage.ActingUserId.HasValue)
{
context.ActingUser = await userRepository.GetByIdAsync(eventMessage.ActingUserId.Value);
}
if (IntegrationTemplateProcessor.TemplateRequiresOrganization(template) && eventMessage.OrganizationId.HasValue)
{
context.Organization = await organizationRepository.GetByIdAsync(eventMessage.OrganizationId.Value);
}
return context;
}
protected abstract IntegrationType GetIntegrationType();
protected abstract Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, string renderedTemplate);
}

View File

@ -13,7 +13,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Constants;
@ -31,12 +30,10 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Logging;
using Stripe;
using OrganizationUserInvite = Bit.Core.Models.Business.OrganizationUserInvite;
@ -77,8 +74,6 @@ public class OrganizationService : IOrganizationService
private readonly IPricingClient _pricingClient;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand;
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
private readonly IDataProtector _dataProtector;
public OrganizationService(
IOrganizationRepository organizationRepository,
@ -112,9 +107,7 @@ public class OrganizationService : IOrganizationService
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient,
IPolicyRequirementQuery policyRequirementQuery,
ISendOrganizationInvitesCommand sendOrganizationInvitesCommand,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IDataProtectionProvider dataProtectionProvider
ISendOrganizationInvitesCommand sendOrganizationInvitesCommand
)
{
_organizationRepository = organizationRepository;
@ -149,8 +142,6 @@ public class OrganizationService : IOrganizationService
_pricingClient = pricingClient;
_policyRequirementQuery = policyRequirementQuery;
_sendOrganizationInvitesCommand = sendOrganizationInvitesCommand;
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
_dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose);
}
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
@ -1008,7 +999,7 @@ public class OrganizationService : IOrganizationService
organization: organization,
initOrganization: initOrganization));
internal async Task<(bool canScale, string failureReason)> CanScaleAsync(
public async Task<(bool canScale, string failureReason)> CanScaleAsync(
Organization organization,
int seatsToAdd)
{
@ -1862,71 +1853,4 @@ public class OrganizationService : IOrganizationService
SalesAssistedTrialStarted = salesAssistedTrialStarted,
});
}
public async Task InitPendingOrganization(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken)
{
await ValidateSignUpPoliciesAsync(user.Id);
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
if (orgUser == null)
{
throw new BadRequestException("User invalid.");
}
// TODO: PM-4142 - remove old token validation logic once 3 releases of backwards compatibility are complete
var newTokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken(
_orgUserInviteTokenDataFactory, emailToken, orgUser);
var tokenValid = newTokenValid ||
CoreHelpers.UserInviteTokenIsValid(_dataProtector, emailToken, user.Email, orgUser.Id,
_globalSettings);
if (!tokenValid)
{
throw new BadRequestException("Invalid token.");
}
var org = await GetOrgById(organizationId);
if (org.Enabled)
{
throw new BadRequestException("Organization is already enabled.");
}
if (org.Status != OrganizationStatusType.Pending)
{
throw new BadRequestException("Organization is not on a Pending status.");
}
if (!string.IsNullOrEmpty(org.PublicKey))
{
throw new BadRequestException("Organization already has a Public Key.");
}
if (!string.IsNullOrEmpty(org.PrivateKey))
{
throw new BadRequestException("Organization already has a Private Key.");
}
org.Enabled = true;
org.Status = OrganizationStatusType.Created;
org.PublicKey = publicKey;
org.PrivateKey = privateKey;
await UpdateAsync(org);
if (!string.IsNullOrWhiteSpace(collectionName))
{
// give the owner Can Manage access over the default collection
List<CollectionAccessSelection> defaultOwnerAccess =
[new CollectionAccessSelection { Id = organizationUserId, HidePasswords = false, ReadOnly = false, Manage = true }];
var defaultCollection = new Collection
{
Name = collectionName,
OrganizationId = org.Id
};
await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess);
}
}
}

View File

@ -1,46 +1,35 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Utilities;
using System.Text.Json.Nodes;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Integrations;
using Bit.Core.Repositories;
#nullable enable
namespace Bit.Core.Services;
public class SlackEventHandler(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationIntegrationConfigurationRepository configurationRepository,
ISlackService slackService)
: IEventMessageHandler
: IntegrationEventHandlerBase(userRepository, organizationRepository, configurationRepository)
{
public async Task HandleEventAsync(EventMessage eventMessage)
protected override IntegrationType GetIntegrationType() => IntegrationType.Slack;
protected override async Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration,
string renderedTemplate)
{
var organizationId = eventMessage.OrganizationId ?? Guid.Empty;
var configurations = await configurationRepository.GetConfigurationDetailsAsync(
organizationId,
IntegrationType.Slack,
eventMessage.Type);
foreach (var configuration in configurations)
var config = mergedConfiguration.Deserialize<SlackIntegrationConfigurationDetails>();
if (config is null)
{
var config = configuration.MergedConfiguration.Deserialize<SlackIntegrationConfigurationDetails>();
if (config is null)
{
continue;
}
await slackService.SendSlackMessageByChannelIdAsync(
config.token,
IntegrationTemplateProcessor.ReplaceTokens(configuration.Template, eventMessage),
config.channelId
);
return;
}
}
public async Task HandleManyEventsAsync(IEnumerable<EventMessage> eventMessages)
{
foreach (var eventMessage in eventMessages)
{
await HandleEventAsync(eventMessage);
}
await slackService.SendSlackMessageByChannelIdAsync(
config.token,
renderedTemplate,
config.channelId
);
}
}

View File

@ -1,8 +1,7 @@
using System.Text;
using System.Text.Json;
using Bit.Core.AdminConsole.Utilities;
using System.Text.Json.Nodes;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Integrations;
using Bit.Core.Repositories;
@ -12,46 +11,28 @@ namespace Bit.Core.Services;
public class WebhookEventHandler(
IHttpClientFactory httpClientFactory,
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationIntegrationConfigurationRepository configurationRepository)
: IEventMessageHandler
: IntegrationEventHandlerBase(userRepository, organizationRepository, configurationRepository)
{
private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);
public const string HttpClientName = "WebhookEventHandlerHttpClient";
public async Task HandleEventAsync(EventMessage eventMessage)
protected override IntegrationType GetIntegrationType() => IntegrationType.Webhook;
protected override async Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration,
string renderedTemplate)
{
var organizationId = eventMessage.OrganizationId ?? Guid.Empty;
var configurations = await configurationRepository.GetConfigurationDetailsAsync(
organizationId,
IntegrationType.Webhook,
eventMessage.Type);
foreach (var configuration in configurations)
var config = mergedConfiguration.Deserialize<WebhookIntegrationConfigurationDetils>();
if (config is null || string.IsNullOrEmpty(config.url))
{
var config = configuration.MergedConfiguration.Deserialize<WebhookIntegrationConfigurationDetils>();
if (config is null || string.IsNullOrEmpty(config.url))
{
continue;
}
var content = new StringContent(
IntegrationTemplateProcessor.ReplaceTokens(configuration.Template, eventMessage),
Encoding.UTF8,
"application/json"
);
var response = await _httpClient.PostAsync(
config.url,
content);
response.EnsureSuccessStatusCode();
return;
}
}
public async Task HandleManyEventsAsync(IEnumerable<EventMessage> eventMessages)
{
foreach (var eventMessage in eventMessages)
{
await HandleEventAsync(eventMessage);
}
var content = new StringContent(renderedTemplate, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(config.url, content);
response.EnsureSuccessStatusCode();
}
}

View File

@ -1,5 +1,6 @@
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Models.Business.Provider;
using Bit.Core.Billing.Models;
using Bit.Core.Entities;
using Bit.Core.Models.Business;
@ -7,7 +8,7 @@ namespace Bit.Core.AdminConsole.Services.NoopImplementations;
public class NoopProviderService : IProviderService
{
public Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo = null) => throw new NotImplementedException();
public Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null) => throw new NotImplementedException();
public Task UpdateAsync(Provider provider, bool updateBilling = false) => throw new NotImplementedException();

View File

@ -10,8 +10,9 @@ public static partial class IntegrationTemplateProcessor
public static string ReplaceTokens(string template, object values)
{
if (string.IsNullOrEmpty(template) || values == null)
{
return template;
}
var type = values.GetType();
return TokenRegex().Replace(template, match =>
{
@ -20,4 +21,36 @@ public static partial class IntegrationTemplateProcessor
return property?.GetValue(values)?.ToString() ?? match.Value;
});
}
public static bool TemplateRequiresUser(string template)
{
if (string.IsNullOrEmpty(template))
{
return false;
}
return template.Contains("#UserName#", StringComparison.Ordinal)
|| template.Contains("#UserEmail#", StringComparison.Ordinal);
}
public static bool TemplateRequiresActingUser(string template)
{
if (string.IsNullOrEmpty(template))
{
return false;
}
return template.Contains("#ActingUserName#", StringComparison.Ordinal)
|| template.Contains("#ActingUserEmail#", StringComparison.Ordinal);
}
public static bool TemplateRequiresOrganization(string template)
{
if (string.IsNullOrEmpty(template))
{
return false;
}
return template.Contains("#OrganizationName#", StringComparison.Ordinal);
}
}

View File

@ -2,6 +2,10 @@
public static class StripeConstants
{
public static class Prices
{
public const string StoragePlanPersonal = "personal-storage-gb-annually";
}
public static class AutomaticTaxStatus
{
public const string Failed = "failed";
@ -42,10 +46,12 @@ public static class StripeConstants
{
public const string Draft = "draft";
public const string Open = "open";
public const string Paid = "paid";
}
public static class MetadataKeys
{
public const string BraintreeCustomerId = "btCustomerId";
public const string InvoiceApproved = "invoice_approved";
public const string OrganizationId = "organizationId";
public const string ProviderId = "providerId";

View File

@ -34,6 +34,7 @@ public static class OrganizationLicenseConstants
public const string UseSecretsManager = nameof(UseSecretsManager);
public const string SmSeats = nameof(SmSeats);
public const string SmServiceAccounts = nameof(SmServiceAccounts);
public const string SmMaxProjects = nameof(SmMaxProjects);
public const string LimitCollectionCreationDeletion = nameof(LimitCollectionCreationDeletion);
public const string AllowAdminAccessToAllCollectionItems = nameof(AllowAdminAccessToAllCollectionItems);
public const string UseRiskInsights = nameof(UseRiskInsights);

View File

@ -7,4 +7,5 @@ public class LicenseContext
{
public Guid? InstallationId { get; init; }
public required SubscriptionInfo SubscriptionInfo { get; init; }
public int? SmMaxProjects { get; set; }
}

View File

@ -112,6 +112,11 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organizati
}
claims.Add(new Claim(nameof(OrganizationLicenseConstants.UseAdminSponsoredFamilies), entity.UseAdminSponsoredFamilies.ToString()));
if (licenseContext.SmMaxProjects.HasValue)
{
claims.Add(new Claim(nameof(OrganizationLicenseConstants.SmMaxProjects), licenseContext.SmMaxProjects.ToString()));
}
return Task.FromResult(claims);
}

View File

@ -38,7 +38,7 @@ public record Families2019Plan : Plan
HasPremiumAccessOption = true;
StripePlanId = "personal-org-annually";
StripeStoragePlanId = "storage-gb-annually";
StripeStoragePlanId = "personal-storage-gb-annually";
StripePremiumAccessPlanId = "personal-org-premium-access-annually";
BasePrice = 12;
AdditionalStoragePricePerGb = 4;

View File

@ -37,7 +37,7 @@ public record FamiliesPlan : Plan
HasAdditionalStorageOption = true;
StripePlanId = "2020-families-org-annually";
StripeStoragePlanId = "storage-gb-annually";
StripeStoragePlanId = "personal-storage-gb-annually";
BasePrice = 40;
AdditionalStoragePricePerGb = 4;

View File

@ -79,10 +79,12 @@ public interface IProviderBillingService
/// </summary>
/// <param name="provider">The <see cref="Provider"/> to create a Stripe customer for.</param>
/// <param name="taxInfo">The <see cref="TaxInfo"/> to use for calculating the customer's automatic tax.</param>
/// <param name="tokenizedPaymentSource">The <see cref="TokenizedPaymentSource"/> (ex. Credit Card) to attach to the customer.</param>
/// <returns>The newly created <see cref="Stripe.Customer"/> for the <paramref name="provider"/>.</returns>
Task<Customer> SetupCustomer(
Provider provider,
TaxInfo taxInfo);
TaxInfo taxInfo,
TokenizedPaymentSource tokenizedPaymentSource = null);
/// <summary>
/// For use during the provider setup process, this method starts a Stripe <see cref="Stripe.Subscription"/> for the given <paramref name="provider"/>.

View File

@ -313,7 +313,7 @@ public class PremiumUserBillingService(
{
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
{
Price = "storage-gb-annually",
Price = StripeConstants.Prices.StoragePlanPersonal,
Quantity = storage
});
}

View File

@ -149,6 +149,8 @@ public static class FeatureFlagKeys
public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates";
public const string PM18770_EnableOrganizationBusinessUnitConversion = "pm-18770-enable-organization-business-unit-conversion";
public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically";
public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup";
public const string UseOrganizationWarningsService = "use-organization-warnings-service";
/* Data Insights and Reporting Team */
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";

View File

@ -0,0 +1,37 @@
using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations.OrganizationSponsorships;
namespace Bit.Core.Models.Api.Response.OrganizationSponsorships;
public class OrganizationSponsorshipInvitesResponseModel : ResponseModel
{
public OrganizationSponsorshipInvitesResponseModel(OrganizationSponsorshipData sponsorshipData, string obj = "organizationSponsorship") : base(obj)
{
if (sponsorshipData == null)
{
throw new ArgumentNullException(nameof(sponsorshipData));
}
SponsoringOrganizationUserId = sponsorshipData.SponsoringOrganizationUserId;
FriendlyName = sponsorshipData.FriendlyName;
OfferedToEmail = sponsorshipData.OfferedToEmail;
PlanSponsorshipType = sponsorshipData.PlanSponsorshipType;
LastSyncDate = sponsorshipData.LastSyncDate;
ValidUntil = sponsorshipData.ValidUntil;
ToDelete = sponsorshipData.ToDelete;
IsAdminInitiated = sponsorshipData.IsAdminInitiated;
Notes = sponsorshipData.Notes;
CloudSponsorshipRemoved = sponsorshipData.CloudSponsorshipRemoved;
}
public Guid SponsoringOrganizationUserId { get; set; }
public string FriendlyName { get; set; }
public string OfferedToEmail { get; set; }
public PlanSponsorshipType PlanSponsorshipType { get; set; }
public DateTime? LastSyncDate { get; set; }
public DateTime? ValidUntil { get; set; }
public bool ToDelete { get; set; }
public bool IsAdminInitiated { get; set; }
public string Notes { get; set; }
public bool CloudSponsorshipRemoved { get; set; }
}

View File

@ -1,5 +1,6 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Pricing;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
@ -16,19 +17,22 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer
private readonly ILicensingService _licensingService;
private readonly IProviderRepository _providerRepository;
private readonly IFeatureService _featureService;
private readonly IPricingClient _pricingClient;
public CloudGetOrganizationLicenseQuery(
IInstallationRepository installationRepository,
IPaymentService paymentService,
ILicensingService licensingService,
IProviderRepository providerRepository,
IFeatureService featureService)
IFeatureService featureService,
IPricingClient pricingClient)
{
_installationRepository = installationRepository;
_paymentService = paymentService;
_licensingService = licensingService;
_providerRepository = providerRepository;
_featureService = featureService;
_pricingClient = pricingClient;
}
public async Task<OrganizationLicense> GetLicenseAsync(Organization organization, Guid installationId,
@ -42,7 +46,11 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer
var subscriptionInfo = await GetSubscriptionAsync(organization);
var license = new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version);
license.Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo);
var plan = await _pricingClient.GetPlan(organization.PlanType);
int? smMaxProjects = plan?.SupportsSecretsManager ?? false
? plan.SecretsManager.MaxProjects
: null;
license.Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo, smMaxProjects);
return license;
}

View File

@ -196,6 +196,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IInviteUsersOrganizationValidator, InviteUsersOrganizationValidator>();
services.AddScoped<IInviteUsersPasswordManagerValidator, InviteUsersPasswordManagerValidator>();
services.AddScoped<IInviteUsersEnvironmentValidator, InviteUsersEnvironmentValidator>();
services.AddScoped<IInitPendingOrganizationCommand, InitPendingOrganizationCommand>();
}
// TODO: move to OrganizationSubscriptionServiceCollectionExtensions when OrganizationUser methods are moved out of

View File

@ -15,7 +15,8 @@ public class CreateSponsorshipCommand(
ICurrentContext currentContext,
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IUserService userService,
IOrganizationService organizationService) : ICreateSponsorshipCommand
IOrganizationService organizationService,
IOrganizationUserRepository organizationUserRepository) : ICreateSponsorshipCommand
{
public async Task<OrganizationSponsorship> CreateSponsorshipAsync(
Organization sponsoringOrganization,
@ -47,11 +48,12 @@ public class CreateSponsorshipCommand(
throw new BadRequestException("Only confirmed users can sponsor other organizations.");
}
var existingOrgSponsorship = await organizationSponsorshipRepository
.GetBySponsoringOrganizationUserIdAsync(sponsoringMember.Id);
if (existingOrgSponsorship?.SponsoredOrganizationId != null)
var sponsorships =
await organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrganization.Id);
var existingSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName == friendlyName);
if (existingSponsorship != null)
{
throw new BadRequestException("Can only sponsor one organization per Organization User.");
return existingSponsorship;
}
if (isAdminInitiated)
@ -70,15 +72,37 @@ public class CreateSponsorshipCommand(
Notes = notes
};
if (existingOrgSponsorship != null)
if (!isAdminInitiated)
{
// Replace existing invalid offer with our new sponsorship offer
sponsorship.Id = existingOrgSponsorship.Id;
var existingOrgSponsorship = await organizationSponsorshipRepository
.GetBySponsoringOrganizationUserIdAsync(sponsoringMember.Id);
if (existingOrgSponsorship?.SponsoredOrganizationId != null)
{
throw new BadRequestException("Can only sponsor one organization per Organization User.");
}
if (existingOrgSponsorship != null)
{
sponsorship.Id = existingOrgSponsorship.Id;
}
}
if (isAdminInitiated && sponsoringOrganization.Seats.HasValue)
{
await organizationService.AutoAddSeatsAsync(sponsoringOrganization, 1);
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrganization.Id);
var availableSeats = sponsoringOrganization.Seats.Value - occupiedSeats;
if (availableSeats <= 0)
{
var newSeatsRequired = 1;
var (canScale, failureReason) = await organizationService.CanScaleAsync(sponsoringOrganization, newSeatsRequired);
if (!canScale)
{
throw new BadRequestException(failureReason);
}
await organizationService.AutoAddSeatsAsync(sponsoringOrganization, newSeatsRequired);
}
}
try

View File

@ -39,7 +39,7 @@ public class AzurePhishingDomainStorageService
var content = await streamReader.ReadToEndAsync();
return [.. content
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim())
.Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith('#'))];
}

View File

@ -92,7 +92,7 @@ public class CloudPhishingDomainDirectQuery : ICloudPhishingDomainQuery
}
return content
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim())
.Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith("#"))
.ToList();

View File

@ -21,7 +21,8 @@ public interface ILicensingService
Task<string?> CreateOrganizationTokenAsync(
Organization organization,
Guid installationId,
SubscriptionInfo subscriptionInfo);
SubscriptionInfo subscriptionInfo,
int? smMaxProjects);
Task<string?> CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo);
}

View File

@ -339,12 +339,13 @@ public class LicensingService : ILicensingService
}
}
public async Task<string> CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo)
public async Task<string> CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo, int? smMaxProjects)
{
var licenseContext = new LicenseContext
{
InstallationId = installationId,
SubscriptionInfo = subscriptionInfo,
SmMaxProjects = smMaxProjects
};
var claims = await _organizationLicenseClaimsFactory.GenerateClaims(organization, licenseContext);

View File

@ -11,6 +11,7 @@ using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Services;
@ -45,7 +46,6 @@ namespace Bit.Core.Services;
public class UserService : UserManager<User>, IUserService, IDisposable
{
private const string PremiumPlanId = "premium-annually";
private const string StoragePlanId = "storage-gb-annually";
private readonly IUserRepository _userRepository;
private readonly ICipherRepository _cipherRepository;
@ -1106,12 +1106,12 @@ public class UserService : UserManager<User>, IUserService, IDisposable
}
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb,
StoragePlanId);
StripeConstants.Prices.StoragePlanPersonal);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.AdjustStorage, user, _currentContext)
{
Storage = storageAdjustmentGb,
PlanName = StoragePlanId,
PlanName = StripeConstants.Prices.StoragePlanPersonal,
});
await SaveUserAsync(user);
return secret;

View File

@ -62,7 +62,7 @@ public class NoopLicensingService : ILicensingService
return null;
}
public Task<string?> CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo)
public Task<string?> CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo, int? smMaxProjects)
{
return Task.FromResult<string?>(null);
}

View File

@ -379,7 +379,7 @@ public class CipherService : ICipherService
if (!valid || realSize > MAX_FILE_SIZE)
{
// File reported differs in size from that promised. Must be a rogue client. Delete Send
await DeleteAttachmentAsync(cipher, attachmentData);
await DeleteAttachmentAsync(cipher, attachmentData, false);
return false;
}
// Update Send data if necessary
@ -483,7 +483,7 @@ public class CipherService : ICipherService
throw new NotFoundException();
}
return await DeleteAttachmentAsync(cipher, cipher.GetAttachments()[attachmentId]);
return await DeleteAttachmentAsync(cipher, cipher.GetAttachments()[attachmentId], orgAdmin);
}
public async Task PurgeAsync(Guid organizationId)
@ -877,7 +877,7 @@ public class CipherService : ICipherService
}
}
private async Task<DeleteAttachmentResponseData> DeleteAttachmentAsync(Cipher cipher, CipherAttachment.MetaData attachmentData)
private async Task<DeleteAttachmentResponseData> DeleteAttachmentAsync(Cipher cipher, CipherAttachment.MetaData attachmentData, bool orgAdmin)
{
if (attachmentData == null || string.IsNullOrWhiteSpace(attachmentData.AttachmentId))
{
@ -891,7 +891,7 @@ public class CipherService : ICipherService
// Update the revision date when an attachment is deleted
cipher.RevisionDate = DateTime.UtcNow;
await _cipherRepository.ReplaceAsync((CipherDetails)cipher);
await _cipherRepository.ReplaceAsync(orgAdmin ? cipher : (CipherDetails)cipher);
// push
await _pushService.PushSyncCipherUpdateAsync(cipher, null);