mirror of
https://github.com/bitwarden/server
synced 2026-01-02 08:33:48 +00:00
Merge branch 'main' into auth/pm-22975/client-version-validator
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
},
|
||||
"storage": {
|
||||
"connectionString": "UseDevelopmentStorage=true"
|
||||
}
|
||||
},
|
||||
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Results;
|
||||
using Bit.Core.Models.Api;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Controllers;
|
||||
|
||||
public abstract class BaseAdminConsoleController : Controller
|
||||
{
|
||||
protected static IResult Handle(CommandResult commandResult) =>
|
||||
commandResult.Match<IResult>(
|
||||
error => error switch
|
||||
{
|
||||
BadRequestError badRequest => TypedResults.BadRequest(new ErrorResponseModel(badRequest.Message)),
|
||||
NotFoundError notFound => TypedResults.NotFound(new ErrorResponseModel(notFound.Message)),
|
||||
InternalError internalError => TypedResults.Json(
|
||||
new ErrorResponseModel(internalError.Message),
|
||||
statusCode: StatusCodes.Status500InternalServerError),
|
||||
_ => TypedResults.Json(
|
||||
new ErrorResponseModel(error.Message),
|
||||
statusCode: StatusCodes.Status500InternalServerError
|
||||
)
|
||||
},
|
||||
_ => TypedResults.NoContent()
|
||||
);
|
||||
}
|
||||
@@ -11,8 +11,10 @@ using Bit.Api.Models.Response;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
@@ -20,6 +22,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
@@ -43,7 +46,7 @@ namespace Bit.Api.AdminConsole.Controllers;
|
||||
|
||||
[Route("organizations/{orgId}/users")]
|
||||
[Authorize("Application")]
|
||||
public class OrganizationUsersController : Controller
|
||||
public class OrganizationUsersController : BaseAdminConsoleController
|
||||
{
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
@@ -68,6 +71,7 @@ public class OrganizationUsersController : Controller
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
|
||||
private readonly IAutomaticallyConfirmOrganizationUserCommand _automaticallyConfirmOrganizationUserCommand;
|
||||
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
|
||||
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||
private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand;
|
||||
@@ -101,7 +105,8 @@ public class OrganizationUsersController : Controller
|
||||
IInitPendingOrganizationCommand initPendingOrganizationCommand,
|
||||
IRevokeOrganizationUserCommand revokeOrganizationUserCommand,
|
||||
IResendOrganizationInviteCommand resendOrganizationInviteCommand,
|
||||
IAdminRecoverAccountCommand adminRecoverAccountCommand)
|
||||
IAdminRecoverAccountCommand adminRecoverAccountCommand,
|
||||
IAutomaticallyConfirmOrganizationUserCommand automaticallyConfirmOrganizationUserCommand)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@@ -126,6 +131,7 @@ public class OrganizationUsersController : Controller
|
||||
_featureService = featureService;
|
||||
_pricingClient = pricingClient;
|
||||
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
|
||||
_automaticallyConfirmOrganizationUserCommand = automaticallyConfirmOrganizationUserCommand;
|
||||
_confirmOrganizationUserCommand = confirmOrganizationUserCommand;
|
||||
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||
_initPendingOrganizationCommand = initPendingOrganizationCommand;
|
||||
@@ -738,6 +744,31 @@ public class OrganizationUsersController : Controller
|
||||
await BulkEnableSecretsManagerAsync(orgId, model);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/auto-confirm")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
[RequireFeature(FeatureFlagKeys.AutomaticConfirmUsers)]
|
||||
public async Task<IResult> AutomaticallyConfirmOrganizationUserAsync([FromRoute] Guid orgId,
|
||||
[FromRoute] Guid id,
|
||||
[FromBody] OrganizationUserConfirmRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
|
||||
if (userId is null || userId.Value == Guid.Empty)
|
||||
{
|
||||
return TypedResults.Unauthorized();
|
||||
}
|
||||
|
||||
return Handle(await _automaticallyConfirmOrganizationUserCommand.AutomaticallyConfirmOrganizationUserAsync(
|
||||
new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationId = orgId,
|
||||
OrganizationUserId = id,
|
||||
Key = model.Key,
|
||||
DefaultUserCollectionName = model.DefaultUserCollectionName,
|
||||
PerformedBy = new StandardUser(userId.Value, await _currentContext.OrganizationOwner(orgId)),
|
||||
}));
|
||||
}
|
||||
|
||||
private async Task RestoreOrRevokeUserAsync(
|
||||
Guid orgId,
|
||||
Guid id,
|
||||
|
||||
@@ -30,6 +30,7 @@ public class PolicyResponseModel : ResponseModel
|
||||
{
|
||||
Data = JsonSerializer.Deserialize<Dictionary<string, object>>(policy.Data);
|
||||
}
|
||||
RevisionDate = policy.RevisionDate;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
@@ -37,4 +38,5 @@ public class PolicyResponseModel : ResponseModel
|
||||
public PolicyType Type { get; set; }
|
||||
public Dictionary<string, object> Data { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public DateTime RevisionDate { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Utilities;
|
||||
@@ -27,7 +28,7 @@ public class ProfileOrganizationResponseModel : BaseProfileOrganizationResponseM
|
||||
FamilySponsorshipToDelete = organizationDetails.FamilySponsorshipToDelete;
|
||||
FamilySponsorshipValidUntil = organizationDetails.FamilySponsorshipValidUntil;
|
||||
FamilySponsorshipAvailable = (organizationDetails.FamilySponsorshipFriendlyName == null || IsAdminInitiated) &&
|
||||
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
|
||||
SponsoredPlans.Get(PlanSponsorshipType.FamiliesForEnterprise)
|
||||
.UsersCanSponsor(organizationDetails);
|
||||
AccessSecretsManager = organizationDetails.AccessSecretsManager;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using Bit.Api.AdminConsole.Authorization;
|
||||
using Bit.Api.AdminConsole.Authorization.Requirements;
|
||||
using Bit.Api.Billing.Attributes;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Organizations.Queries;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace Bit.Api.Billing.Controllers.VNext;
|
||||
|
||||
[Authorize("Application")]
|
||||
[Route("organizations/{organizationId:guid}/billing/vnext/self-host")]
|
||||
[SelfHosted(SelfHostedOnly = true)]
|
||||
public class SelfHostedBillingController(
|
||||
IGetOrganizationMetadataQuery getOrganizationMetadataQuery) : BaseBillingController
|
||||
{
|
||||
[Authorize<MemberOrProviderRequirement>]
|
||||
[HttpGet("metadata")]
|
||||
[RequireFeature(FeatureFlagKeys.PM25379_UseNewOrganizationMetadataStructure)]
|
||||
[InjectOrganization]
|
||||
public async Task<IResult> GetMetadataAsync([BindNever] Organization organization)
|
||||
{
|
||||
var metadata = await getOrganizationMetadataQuery.Run(organization);
|
||||
|
||||
if (metadata == null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
return TypedResults.Ok(metadata);
|
||||
}
|
||||
}
|
||||
@@ -1422,11 +1422,9 @@ public class CiphersController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Extract lastKnownRevisionDate from form data if present
|
||||
DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm();
|
||||
await Request.GetFileAsync(async (stream) =>
|
||||
{
|
||||
await _cipherService.UploadFileForExistingAttachmentAsync(stream, cipher, attachmentData, lastKnownRevisionDate);
|
||||
await _cipherService.UploadFileForExistingAttachmentAsync(stream, cipher, attachmentData);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1525,13 +1523,10 @@ public class CiphersController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Extract lastKnownRevisionDate from form data if present
|
||||
DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm();
|
||||
|
||||
await Request.GetFileAsync(async (stream, fileName, key) =>
|
||||
{
|
||||
await _cipherService.CreateAttachmentShareAsync(cipher, stream, fileName, key,
|
||||
Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId, lastKnownRevisionDate);
|
||||
Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"phishingDomain": {
|
||||
"updateUrl": "https://phish.co.za/latest/phishing-domains-ACTIVE.txt",
|
||||
"checksumUrl": "https://raw.githubusercontent.com/Phishing-Database/checksums/refs/heads/master/phishing-domains-ACTIVE.txt.sha256"
|
||||
}
|
||||
},
|
||||
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
|
||||
using Bit.Billing.Constants;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Braintree;
|
||||
using Stripe;
|
||||
using Customer = Stripe.Customer;
|
||||
@@ -112,7 +112,7 @@ public class StripeEventUtilityService : IStripeEventUtilityService
|
||||
}
|
||||
|
||||
public bool IsSponsoredSubscription(Subscription subscription) =>
|
||||
StaticStore.SponsoredPlans
|
||||
SponsoredPlans.All
|
||||
.Any(p => subscription.Items
|
||||
.Any(i => i.Plan.Id == p.StripePlanId));
|
||||
|
||||
|
||||
@@ -195,41 +195,48 @@ public class UpcomingInvoiceHandler(
|
||||
Plan plan,
|
||||
bool milestone3)
|
||||
{
|
||||
if (milestone3 && plan.Type == PlanType.FamiliesAnnually2019)
|
||||
// currently these are the only plans that need aligned and both require the same flag and share most of the logic
|
||||
if (!milestone3 || plan.Type is not (PlanType.FamiliesAnnually2019 or PlanType.FamiliesAnnually2025))
|
||||
{
|
||||
var passwordManagerItem =
|
||||
subscription.Items.FirstOrDefault(item => item.Price.Id == plan.PasswordManager.StripePlanId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordManagerItem == null)
|
||||
{
|
||||
logger.LogWarning("Could not find Organization's ({OrganizationId}) password manager item while processing '{EventType}' event ({EventID})",
|
||||
organization.Id, @event.Type, @event.Id);
|
||||
return;
|
||||
}
|
||||
var passwordManagerItem =
|
||||
subscription.Items.FirstOrDefault(item => item.Price.Id == plan.PasswordManager.StripePlanId);
|
||||
|
||||
var families = await pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually);
|
||||
if (passwordManagerItem == null)
|
||||
{
|
||||
logger.LogWarning("Could not find Organization's ({OrganizationId}) password manager item while processing '{EventType}' event ({EventID})",
|
||||
organization.Id, @event.Type, @event.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
organization.PlanType = families.Type;
|
||||
organization.Plan = families.Name;
|
||||
organization.UsersGetPremium = families.UsersGetPremium;
|
||||
organization.Seats = families.PasswordManager.BaseSeats;
|
||||
var families = await pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually);
|
||||
|
||||
var options = new SubscriptionUpdateOptions
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new SubscriptionItemOptions
|
||||
{
|
||||
Id = passwordManagerItem.Id,
|
||||
organization.PlanType = families.Type;
|
||||
organization.Plan = families.Name;
|
||||
organization.UsersGetPremium = families.UsersGetPremium;
|
||||
organization.Seats = families.PasswordManager.BaseSeats;
|
||||
|
||||
var options = new SubscriptionUpdateOptions
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new SubscriptionItemOptions
|
||||
{
|
||||
Id = passwordManagerItem.Id,
|
||||
Price = families.PasswordManager.StripePlanId
|
||||
}
|
||||
],
|
||||
Discounts =
|
||||
[
|
||||
new SubscriptionDiscountOptions { Coupon = CouponIDs.Milestone3SubscriptionDiscount }
|
||||
],
|
||||
ProrationBehavior = ProrationBehavior.None
|
||||
};
|
||||
}
|
||||
],
|
||||
ProrationBehavior = ProrationBehavior.None
|
||||
};
|
||||
|
||||
if (plan.Type == PlanType.FamiliesAnnually2019)
|
||||
{
|
||||
options.Discounts =
|
||||
[
|
||||
new SubscriptionDiscountOptions { Coupon = CouponIDs.Milestone3SubscriptionDiscount }
|
||||
];
|
||||
|
||||
var premiumAccessAddOnItem = subscription.Items.FirstOrDefault(item =>
|
||||
item.Price.Id == plan.PasswordManager.StripePremiumAccessPlanId);
|
||||
@@ -253,21 +260,21 @@ public class UpcomingInvoiceHandler(
|
||||
Deleted = true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await organizationRepository.ReplaceAsync(organization);
|
||||
await stripeFacade.UpdateSubscription(subscription.Id, options);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(
|
||||
exception,
|
||||
"Failed to align subscription concerns for Organization ({OrganizationID}) while processing '{EventType}' event ({EventID})",
|
||||
organization.Id,
|
||||
@event.Type,
|
||||
@event.Id);
|
||||
}
|
||||
try
|
||||
{
|
||||
await organizationRepository.ReplaceAsync(organization);
|
||||
await stripeFacade.UpdateSubscription(subscription.Id, options);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(
|
||||
exception,
|
||||
"Failed to align subscription concerns for Organization ({OrganizationID}) while processing '{EventType}' event ({EventID})",
|
||||
organization.Id,
|
||||
@event.Type,
|
||||
@event.Id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"billingSettings": {
|
||||
"onyx": {
|
||||
"personaId": 68
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ public enum EventType : int
|
||||
OrganizationUser_RejectedAuthRequest = 1514,
|
||||
OrganizationUser_Deleted = 1515, // Both user and organization user data were deleted
|
||||
OrganizationUser_Left = 1516, // User voluntarily left the organization
|
||||
OrganizationUser_AutomaticallyConfirmed = 1517,
|
||||
|
||||
Organization_Updated = 1600,
|
||||
Organization_PurgedVault = 1601,
|
||||
|
||||
@@ -21,6 +21,7 @@ public enum PolicyType : byte
|
||||
UriMatchDefaults = 16,
|
||||
AutotypeDefaultSetting = 17,
|
||||
AutomaticUserConfirmation = 18,
|
||||
BlockClaimedDomainAccountCreation = 19,
|
||||
}
|
||||
|
||||
public static class PolicyTypeExtensions
|
||||
@@ -52,6 +53,7 @@ public static class PolicyTypeExtensions
|
||||
PolicyType.UriMatchDefaults => "URI match defaults",
|
||||
PolicyType.AutotypeDefaultSetting => "Autotype default setting",
|
||||
PolicyType.AutomaticUserConfirmation => "Automatically confirm invited users",
|
||||
PolicyType.BlockClaimedDomainAccountCreation => "Block account creation for claimed domains",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
|
||||
|
||||
public record AcceptedOrganizationUserToConfirm
|
||||
{
|
||||
public required Guid OrganizationUserId { get; init; }
|
||||
public required Guid UserId { get; init; }
|
||||
public required string Key { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OneOf.Types;
|
||||
using CommandResult = Bit.Core.AdminConsole.Utilities.v2.Results.CommandResult;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
|
||||
public class AutomaticallyConfirmOrganizationUserCommand(IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IAutomaticallyConfirmOrganizationUsersValidator validator,
|
||||
IEventService eventService,
|
||||
IMailService mailService,
|
||||
IUserRepository userRepository,
|
||||
IPushRegistrationService pushRegistrationService,
|
||||
IDeviceRepository deviceRepository,
|
||||
IPushNotificationService pushNotificationService,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
ICollectionRepository collectionRepository,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<AutomaticallyConfirmOrganizationUserCommand> logger) : IAutomaticallyConfirmOrganizationUserCommand
|
||||
{
|
||||
public async Task<CommandResult> AutomaticallyConfirmOrganizationUserAsync(AutomaticallyConfirmOrganizationUserRequest request)
|
||||
{
|
||||
var validatorRequest = await RetrieveDataAsync(request);
|
||||
|
||||
var validatedData = await validator.ValidateAsync(validatorRequest);
|
||||
|
||||
return await validatedData.Match<Task<CommandResult>>(
|
||||
error => Task.FromResult(new CommandResult(error)),
|
||||
async _ =>
|
||||
{
|
||||
var userToConfirm = new AcceptedOrganizationUserToConfirm
|
||||
{
|
||||
OrganizationUserId = validatedData.Request.OrganizationUser!.Id,
|
||||
UserId = validatedData.Request.OrganizationUser.UserId!.Value,
|
||||
Key = validatedData.Request.Key
|
||||
};
|
||||
|
||||
// This operation is idempotent. If false, the user is already confirmed and no additional side effects are required.
|
||||
if (!await organizationUserRepository.ConfirmOrganizationUserAsync(userToConfirm))
|
||||
{
|
||||
return new None();
|
||||
}
|
||||
|
||||
await CreateDefaultCollectionsAsync(validatedData.Request);
|
||||
|
||||
await Task.WhenAll(
|
||||
LogOrganizationUserConfirmedEventAsync(validatedData.Request),
|
||||
SendConfirmedOrganizationUserEmailAsync(validatedData.Request),
|
||||
SyncOrganizationKeysAsync(validatedData.Request)
|
||||
);
|
||||
|
||||
return new None();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async Task SyncOrganizationKeysAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
await DeleteDeviceRegistrationAsync(request);
|
||||
await PushSyncOrganizationKeysAsync(request);
|
||||
}
|
||||
|
||||
private async Task CreateDefaultCollectionsAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await ShouldCreateDefaultCollectionAsync(request))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await collectionRepository.CreateAsync(
|
||||
new Collection
|
||||
{
|
||||
OrganizationId = request.Organization!.Id,
|
||||
Name = request.DefaultUserCollectionName,
|
||||
Type = CollectionType.DefaultUserCollection
|
||||
},
|
||||
groups: null,
|
||||
[new CollectionAccessSelection
|
||||
{
|
||||
Id = request.OrganizationUser!.Id,
|
||||
Manage = true
|
||||
}]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to create default collection for user.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a default collection should be created for an organization user during the confirmation process.
|
||||
/// </summary>
|
||||
/// <param name="request">
|
||||
/// The validation request containing information about the user, organization, and collection settings.
|
||||
/// </param>
|
||||
/// <returns>The result is a boolean value indicating whether a default collection should be created.</returns>
|
||||
private async Task<bool> ShouldCreateDefaultCollectionAsync(AutomaticallyConfirmOrganizationUserValidationRequest request) =>
|
||||
!string.IsNullOrWhiteSpace(request.DefaultUserCollectionName)
|
||||
&& (await policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(request.OrganizationUser!.UserId!.Value))
|
||||
.RequiresDefaultCollectionOnConfirm(request.Organization!.Id);
|
||||
|
||||
private async Task PushSyncOrganizationKeysAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
await pushNotificationService.PushSyncOrgKeysAsync(request.OrganizationUser!.UserId!.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to push organization keys.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LogOrganizationUserConfirmedEventAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
await eventService.LogOrganizationUserEventAsync(request.OrganizationUser,
|
||||
EventType.OrganizationUser_AutomaticallyConfirmed,
|
||||
timeProvider.GetUtcNow().UtcDateTime);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to log OrganizationUser_AutomaticallyConfirmed event.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendConfirmedOrganizationUserEmailAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = await userRepository.GetByIdAsync(request.OrganizationUser!.UserId!.Value);
|
||||
|
||||
await mailService.SendOrganizationConfirmedEmailAsync(request.Organization!.Name,
|
||||
user!.Email,
|
||||
request.OrganizationUser.AccessSecretsManager);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to send OrganizationUserConfirmed.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteDeviceRegistrationAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var devices = (await deviceRepository.GetManyByUserIdAsync(request.OrganizationUser!.UserId!.Value))
|
||||
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
||||
.Select(d => d.Id.ToString());
|
||||
|
||||
await pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices, request.Organization!.Id.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to delete device registration.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<AutomaticallyConfirmOrganizationUserValidationRequest> RetrieveDataAsync(
|
||||
AutomaticallyConfirmOrganizationUserRequest request)
|
||||
{
|
||||
return new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
OrganizationUserId = request.OrganizationUserId,
|
||||
OrganizationId = request.OrganizationId,
|
||||
Key = request.Key,
|
||||
DefaultUserCollectionName = request.DefaultUserCollectionName,
|
||||
PerformedBy = request.PerformedBy,
|
||||
OrganizationUser = await organizationUserRepository.GetByIdAsync(request.OrganizationUserId),
|
||||
Organization = await organizationRepository.GetByIdAsync(request.OrganizationId)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
|
||||
/// <summary>
|
||||
/// Automatically Confirm User Command Request
|
||||
/// </summary>
|
||||
public record AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
public required Guid OrganizationUserId { get; init; }
|
||||
public required Guid OrganizationId { get; init; }
|
||||
public required string Key { get; init; }
|
||||
public required string DefaultUserCollectionName { get; init; }
|
||||
public required IActingUser PerformedBy { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Automatically Confirm User Validation Request
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used to hold retrieved data and pass it to the validator
|
||||
/// </remarks>
|
||||
public record AutomaticallyConfirmOrganizationUserValidationRequest : AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
public OrganizationUser? OrganizationUser { get; set; }
|
||||
public Organization? Organization { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
|
||||
public class AutomaticallyConfirmOrganizationUsersValidator(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IPolicyRepository policyRepository) : IAutomaticallyConfirmOrganizationUsersValidator
|
||||
{
|
||||
public async Task<ValidationResult<AutomaticallyConfirmOrganizationUserValidationRequest>> ValidateAsync(
|
||||
AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
// User must exist
|
||||
if (request is { OrganizationUser: null } || request.OrganizationUser is { UserId: null })
|
||||
{
|
||||
return Invalid(request, new UserNotFoundError());
|
||||
}
|
||||
|
||||
// Organization must exist
|
||||
if (request is { Organization: null })
|
||||
{
|
||||
return Invalid(request, new OrganizationNotFound());
|
||||
}
|
||||
|
||||
// User must belong to the organization
|
||||
if (request.OrganizationUser.OrganizationId != request.Organization.Id)
|
||||
{
|
||||
return Invalid(request, new OrganizationUserIdIsInvalid());
|
||||
}
|
||||
|
||||
// User must be accepted
|
||||
if (request is { OrganizationUser.Status: not OrganizationUserStatusType.Accepted })
|
||||
{
|
||||
return Invalid(request, new UserIsNotAccepted());
|
||||
}
|
||||
|
||||
// User must be of type User
|
||||
if (request is { OrganizationUser.Type: not OrganizationUserType.User })
|
||||
{
|
||||
return Invalid(request, new UserIsNotUserType());
|
||||
}
|
||||
|
||||
if (!await OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync(request))
|
||||
{
|
||||
return Invalid(request, new AutomaticallyConfirmUsersPolicyIsNotEnabled());
|
||||
}
|
||||
|
||||
if (!await OrganizationUserConformsToTwoFactorRequiredPolicyAsync(request))
|
||||
{
|
||||
return Invalid(request, new UserDoesNotHaveTwoFactorEnabled());
|
||||
}
|
||||
|
||||
if (await OrganizationUserConformsToSingleOrgPolicyAsync(request) is { } error)
|
||||
{
|
||||
return Invalid(request, error);
|
||||
}
|
||||
|
||||
return Valid(request);
|
||||
}
|
||||
|
||||
private async Task<bool> OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync(
|
||||
AutomaticallyConfirmOrganizationUserValidationRequest request) =>
|
||||
await policyRepository.GetByOrganizationIdTypeAsync(request.OrganizationId,
|
||||
PolicyType.AutomaticUserConfirmation) is { Enabled: true }
|
||||
&& request.Organization is { UseAutomaticUserConfirmation: true };
|
||||
|
||||
private async Task<bool> OrganizationUserConformsToTwoFactorRequiredPolicyAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
if ((await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync([request.OrganizationUser!.UserId!.Value]))
|
||||
.Any(x => x.userId == request.OrganizationUser.UserId && x.twoFactorIsEnabled))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return !(await policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(request.OrganizationUser.UserId!.Value))
|
||||
.IsTwoFactorRequiredForOrganization(request.Organization!.Id);
|
||||
}
|
||||
|
||||
private async Task<Error?> OrganizationUserConformsToSingleOrgPolicyAsync(
|
||||
AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
var allOrganizationUsersForUser = await organizationUserRepository
|
||||
.GetManyByUserAsync(request.OrganizationUser!.UserId!.Value);
|
||||
|
||||
if (allOrganizationUsersForUser.Count == 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var policyRequirement = await policyRequirementQuery
|
||||
.GetAsync<SingleOrganizationPolicyRequirement>(request.OrganizationUser!.UserId!.Value);
|
||||
|
||||
if (policyRequirement.IsSingleOrgEnabledForThisOrganization(request.Organization!.Id))
|
||||
{
|
||||
return new OrganizationEnforcesSingleOrgPolicy();
|
||||
}
|
||||
|
||||
if (policyRequirement.IsSingleOrgEnabledForOrganizationsOtherThan(request.Organization.Id))
|
||||
{
|
||||
return new OtherOrganizationEnforcesSingleOrgPolicy();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
|
||||
public record OrganizationNotFound() : NotFoundError("Invalid organization");
|
||||
public record FailedToWriteToEventLog() : InternalError("Failed to write to event log");
|
||||
public record UserIsNotUserType() : BadRequestError("Only organization users with the User role can be automatically confirmed");
|
||||
public record UserIsNotAccepted() : BadRequestError("Cannot confirm user that has not accepted the invitation.");
|
||||
public record OrganizationUserIdIsInvalid() : BadRequestError("Invalid organization user id.");
|
||||
public record UserDoesNotHaveTwoFactorEnabled() : BadRequestError("User does not have two-step login enabled.");
|
||||
public record OrganizationEnforcesSingleOrgPolicy() : BadRequestError("Cannot confirm this member to the organization until they leave or remove all other organizations");
|
||||
public record OtherOrganizationEnforcesSingleOrgPolicy() : BadRequestError("Cannot confirm this member to the organization because they are in another organization which forbids it.");
|
||||
public record AutomaticallyConfirmUsersPolicyIsNotEnabled() : BadRequestError("Cannot confirm this member because the Automatically Confirm Users policy is not enabled.");
|
||||
@@ -0,0 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
|
||||
public interface IAutomaticallyConfirmOrganizationUsersValidator
|
||||
{
|
||||
Task<ValidationResult<AutomaticallyConfirmOrganizationUserValidationRequest>> ValidateAsync(
|
||||
AutomaticallyConfirmOrganizationUserValidationRequest request);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Results;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount.ValidationResultHelpers;
|
||||
using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
|
||||
/// <summary>
|
||||
/// A strongly typed error containing a reason that an action failed.
|
||||
/// This is used for business logic validation and other expected errors, not exceptions.
|
||||
/// </summary>
|
||||
public abstract record Error(string Message);
|
||||
/// <summary>
|
||||
/// An <see cref="Error"/> type that maps to a NotFoundResult at the api layer.
|
||||
/// </summary>
|
||||
/// <param name="Message"></param>
|
||||
public abstract record NotFoundError(string Message) : Error(Message);
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
|
||||
public record UserNotFoundError() : NotFoundError("Invalid user.");
|
||||
public record UserNotClaimedError() : Error("Member is not claimed by the organization.");
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Results;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
|
||||
public interface IDeleteClaimedOrganizationUserAccountCommand
|
||||
{
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
|
||||
public interface IDeleteClaimedOrganizationUserAccountValidator
|
||||
{
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Results;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Command to automatically confirm an organization user.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The auto-confirm feature enables eligible client apps to confirm OrganizationUsers
|
||||
/// automatically via push notifications, eliminating the need for manual administrator
|
||||
/// intervention. Client apps receive a push notification, perform the required key exchange,
|
||||
/// and submit an auto-confirm request to the server. This command processes those
|
||||
/// client-initiated requests and should only be used in that specific context.
|
||||
/// </remarks>
|
||||
public interface IAutomaticallyConfirmOrganizationUserCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Automatically confirms the organization user based on the provided request data.
|
||||
/// </summary>
|
||||
/// <param name="request">The request containing necessary information to confirm the organization user.</param>
|
||||
/// <remarks>
|
||||
/// This action has side effects. The side effects are
|
||||
/// <ul>
|
||||
/// <li>Creating an event log entry.</li>
|
||||
/// <li>Syncing organization keys with the user.</li>
|
||||
/// <li>Deleting any registered user devices for the organization.</li>
|
||||
/// <li>Sending an email to the confirmed user.</li>
|
||||
/// <li>Creating the default collection if applicable.</li>
|
||||
/// </ul>
|
||||
///
|
||||
/// Each of these actions is performed independently of each other and not guaranteed to be performed in any order.
|
||||
/// Errors will be reported back for the actions that failed in a consolidated error message.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// The result of the command. If there was an error, the result will contain a typed error describing the problem
|
||||
/// that occurred.
|
||||
/// </returns>
|
||||
Task<CommandResult> AutomaticallyConfirmOrganizationUserAsync(AutomaticallyConfirmOrganizationUserRequest request);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
public class SingleOrganizationPolicyRequirement(IEnumerable<PolicyDetails> policyDetails) : IPolicyRequirement
|
||||
{
|
||||
public bool IsSingleOrgEnabledForThisOrganization(Guid organizationId) =>
|
||||
policyDetails.Any(p => p.OrganizationId == organizationId);
|
||||
|
||||
public bool IsSingleOrgEnabledForOrganizationsOtherThan(Guid organizationId) =>
|
||||
policyDetails.Any(p => p.OrganizationId != organizationId);
|
||||
}
|
||||
|
||||
public class SingleOrganizationPolicyRequirementFactory : BasePolicyRequirementFactory<SingleOrganizationPolicyRequirement>
|
||||
{
|
||||
public override PolicyType PolicyType => PolicyType.SingleOrg;
|
||||
|
||||
public override SingleOrganizationPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails) =>
|
||||
new(policyDetails);
|
||||
}
|
||||
@@ -53,6 +53,7 @@ public static class PolicyServiceCollectionExtensions
|
||||
services.AddScoped<IPolicyUpdateEvent, FreeFamiliesForEnterprisePolicyValidator>();
|
||||
services.AddScoped<IPolicyUpdateEvent, OrganizationDataOwnershipPolicyValidator>();
|
||||
services.AddScoped<IPolicyUpdateEvent, UriMatchDefaultPolicyValidator>();
|
||||
services.AddScoped<IPolicyUpdateEvent, BlockClaimedDomainAccountCreationPolicyValidator>();
|
||||
services.AddScoped<IPolicyUpdateEvent, AutomaticUserConfirmationPolicyEventHandler>();
|
||||
}
|
||||
|
||||
@@ -65,5 +66,6 @@ public static class PolicyServiceCollectionExtensions
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireSsoPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireTwoFactorPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, MasterPasswordPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SingleOrganizationPolicyRequirementFactory>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
public class BlockClaimedDomainAccountCreationPolicyValidator : IPolicyValidator, IPolicyValidationEvent
|
||||
{
|
||||
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public BlockClaimedDomainAccountCreationPolicyValidator(
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
public PolicyType Type => PolicyType.BlockClaimedDomainAccountCreation;
|
||||
|
||||
// No prerequisites - this policy stands alone
|
||||
public IEnumerable<PolicyType> RequiredPolicies => [];
|
||||
|
||||
public async Task<string> ValidateAsync(SavePolicyModel policyRequest, Policy? currentPolicy)
|
||||
{
|
||||
return await ValidateAsync(policyRequest.PolicyUpdate, currentPolicy);
|
||||
}
|
||||
|
||||
public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||
{
|
||||
// Check if feature is enabled
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation))
|
||||
{
|
||||
return "This feature is not enabled";
|
||||
}
|
||||
|
||||
// Only validate when trying to ENABLE the policy
|
||||
if (policyUpdate is { Enabled: true })
|
||||
{
|
||||
// Check if organization has at least one verified domain
|
||||
if (!await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))
|
||||
{
|
||||
return "You must claim at least one domain to turn on this policy";
|
||||
}
|
||||
}
|
||||
|
||||
// Disabling the policy is always allowed
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
@@ -29,8 +27,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator, IPolicyValidationEvent
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
|
||||
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
|
||||
|
||||
@@ -40,8 +36,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator, IPolicyValidationEvent
|
||||
IOrganizationRepository organizationRepository,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
|
||||
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
|
||||
{
|
||||
@@ -50,8 +44,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator, IPolicyValidationEvent
|
||||
_organizationRepository = organizationRepository;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_currentContext = currentContext;
|
||||
_featureService = featureService;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -93,7 +94,7 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
||||
///
|
||||
/// This is an idempotent operation.
|
||||
/// </summary>
|
||||
/// <param name="organizationUser">Accepted OrganizationUser to confirm</param>
|
||||
/// <param name="organizationUserToConfirm">Accepted OrganizationUser to confirm</param>
|
||||
/// <returns>True, if the user was updated. False, if not performed.</returns>
|
||||
Task<bool> ConfirmOrganizationUserAsync(OrganizationUser organizationUser);
|
||||
Task<bool> ConfirmOrganizationUserAsync(AcceptedOrganizationUserToConfirm organizationUserToConfirm);
|
||||
}
|
||||
|
||||
15
src/Core/AdminConsole/Utilities/v2/Errors.cs
Normal file
15
src/Core/AdminConsole/Utilities/v2/Errors.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Bit.Core.AdminConsole.Utilities.v2;
|
||||
|
||||
/// <summary>
|
||||
/// A strongly typed error containing a reason that an action failed.
|
||||
/// This is used for business logic validation and other expected errors, not exceptions.
|
||||
/// </summary>
|
||||
public abstract record Error(string Message);
|
||||
/// <summary>
|
||||
/// An <see cref="Error"/> type that maps to a NotFoundResult at the api layer.
|
||||
/// </summary>
|
||||
/// <param name="Message"></param>
|
||||
public abstract record NotFoundError(string Message) : Error(Message);
|
||||
|
||||
public abstract record BadRequestError(string Message) : Error(Message);
|
||||
public abstract record InternalError(string Message) : Error(Message);
|
||||
@@ -1,7 +1,7 @@
|
||||
using OneOf;
|
||||
using OneOf.Types;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
namespace Bit.Core.AdminConsole.Utilities.v2.Results;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a command.
|
||||
@@ -39,4 +39,3 @@ public record BulkCommandResult<T>(Guid Id, CommandResult<T> Result);
|
||||
/// A wrapper for <see cref="CommandResult"/> with an ID, to identify the result in bulk operations.
|
||||
/// </summary>
|
||||
public record BulkCommandResult(Guid Id, CommandResult Result);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using OneOf;
|
||||
using OneOf.Types;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
namespace Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of validating a request.
|
||||
@@ -15,16 +15,20 @@ using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
||||
|
||||
public class RegisterUserCommand : IRegisterUserCommand
|
||||
{
|
||||
private readonly ILogger<RegisterUserCommand> _logger;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;
|
||||
@@ -37,28 +41,32 @@ public class RegisterUserCommand : IRegisterUserCommand
|
||||
private readonly IValidateRedemptionTokenCommand _validateRedemptionTokenCommand;
|
||||
|
||||
private readonly IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> _emergencyAccessInviteTokenDataFactory;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
private readonly string _disabledUserRegistrationExceptionMsg = "Open registration has been disabled by the system administrator.";
|
||||
|
||||
public RegisterUserCommand(
|
||||
ILogger<RegisterUserCommand> logger,
|
||||
IGlobalSettings globalSettings,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
IOrganizationDomainRepository organizationDomainRepository,
|
||||
IFeatureService featureService,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
||||
IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> registrationEmailVerificationTokenDataFactory,
|
||||
IUserService userService,
|
||||
IMailService mailService,
|
||||
IValidateRedemptionTokenCommand validateRedemptionTokenCommand,
|
||||
IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> emergencyAccessInviteTokenDataFactory,
|
||||
IFeatureService featureService)
|
||||
IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> emergencyAccessInviteTokenDataFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_policyRepository = policyRepository;
|
||||
_organizationDomainRepository = organizationDomainRepository;
|
||||
_featureService = featureService;
|
||||
|
||||
_organizationServiceDataProtector = dataProtectionProvider.CreateProtector(
|
||||
"OrganizationServiceDataProtector");
|
||||
@@ -77,6 +85,8 @@ public class RegisterUserCommand : IRegisterUserCommand
|
||||
|
||||
public async Task<IdentityResult> RegisterUser(User user)
|
||||
{
|
||||
await ValidateEmailDomainNotBlockedAsync(user.Email);
|
||||
|
||||
var result = await _userService.CreateUserAsync(user);
|
||||
if (result == IdentityResult.Success)
|
||||
{
|
||||
@@ -102,6 +112,11 @@ public class RegisterUserCommand : IRegisterUserCommand
|
||||
{
|
||||
TryValidateOrgInviteToken(orgInviteToken, orgUserId, user);
|
||||
var orgUser = await SetUserEmail2FaIfOrgPolicyEnabledAsync(orgUserId, user);
|
||||
if (orgUser == null && orgUserId.HasValue)
|
||||
{
|
||||
throw new BadRequestException("Invalid organization user invitation.");
|
||||
}
|
||||
await ValidateEmailDomainNotBlockedAsync(user.Email, orgUser?.OrganizationId);
|
||||
|
||||
user.ApiKey = CoreHelpers.SecureRandomString(30);
|
||||
|
||||
@@ -265,6 +280,8 @@ public class RegisterUserCommand : IRegisterUserCommand
|
||||
string emailVerificationToken)
|
||||
{
|
||||
ValidateOpenRegistrationAllowed();
|
||||
await ValidateEmailDomainNotBlockedAsync(user.Email);
|
||||
|
||||
var tokenable = ValidateRegistrationEmailVerificationTokenable(emailVerificationToken, user.Email);
|
||||
|
||||
user.EmailVerified = true;
|
||||
@@ -284,6 +301,7 @@ public class RegisterUserCommand : IRegisterUserCommand
|
||||
string orgSponsoredFreeFamilyPlanInviteToken)
|
||||
{
|
||||
ValidateOpenRegistrationAllowed();
|
||||
await ValidateEmailDomainNotBlockedAsync(user.Email);
|
||||
await ValidateOrgSponsoredFreeFamilyPlanInviteToken(orgSponsoredFreeFamilyPlanInviteToken, user.Email);
|
||||
|
||||
user.EmailVerified = true;
|
||||
@@ -304,6 +322,7 @@ public class RegisterUserCommand : IRegisterUserCommand
|
||||
string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
|
||||
{
|
||||
ValidateOpenRegistrationAllowed();
|
||||
await ValidateEmailDomainNotBlockedAsync(user.Email);
|
||||
ValidateAcceptEmergencyAccessInviteToken(acceptEmergencyAccessInviteToken, acceptEmergencyAccessId, user.Email);
|
||||
|
||||
user.EmailVerified = true;
|
||||
@@ -322,6 +341,7 @@ public class RegisterUserCommand : IRegisterUserCommand
|
||||
string providerInviteToken, Guid providerUserId)
|
||||
{
|
||||
ValidateOpenRegistrationAllowed();
|
||||
await ValidateEmailDomainNotBlockedAsync(user.Email);
|
||||
ValidateProviderInviteToken(providerInviteToken, providerUserId, user.Email);
|
||||
|
||||
user.EmailVerified = true;
|
||||
@@ -387,6 +407,28 @@ public class RegisterUserCommand : IRegisterUserCommand
|
||||
return tokenable;
|
||||
}
|
||||
|
||||
private async Task ValidateEmailDomainNotBlockedAsync(string email, Guid? excludeOrganizationId = null)
|
||||
{
|
||||
// Only check if feature flag is enabled
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var emailDomain = EmailValidation.GetDomain(email);
|
||||
|
||||
var isDomainBlocked = await _organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(
|
||||
emailDomain, excludeOrganizationId);
|
||||
if (isDomainBlocked)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"User registration blocked by domain claim policy. Domain: {Domain}, ExcludedOrgId: {ExcludedOrgId}",
|
||||
emailDomain,
|
||||
excludeOrganizationId);
|
||||
throw new BadRequestException("This email address is claimed by an organization using Bitwarden.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We send different welcome emails depending on whether the user is joining a free/family or an enterprise organization. If information to populate the
|
||||
/// email isn't present we send the standard individual welcome email.
|
||||
|
||||
@@ -5,6 +5,8 @@ using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
||||
|
||||
@@ -15,25 +17,30 @@ namespace Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
||||
/// </summary>
|
||||
public class SendVerificationEmailForRegistrationCommand : ISendVerificationEmailForRegistrationCommand
|
||||
{
|
||||
|
||||
private readonly ILogger<SendVerificationEmailForRegistrationCommand> _logger;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _tokenDataFactory;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
||||
|
||||
public SendVerificationEmailForRegistrationCommand(
|
||||
ILogger<SendVerificationEmailForRegistrationCommand> logger,
|
||||
IUserRepository userRepository,
|
||||
GlobalSettings globalSettings,
|
||||
IMailService mailService,
|
||||
IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> tokenDataFactory,
|
||||
IFeatureService featureService)
|
||||
IFeatureService featureService,
|
||||
IOrganizationDomainRepository organizationDomainRepository)
|
||||
{
|
||||
_logger = logger;
|
||||
_userRepository = userRepository;
|
||||
_globalSettings = globalSettings;
|
||||
_mailService = mailService;
|
||||
_tokenDataFactory = tokenDataFactory;
|
||||
_featureService = featureService;
|
||||
_organizationDomainRepository = organizationDomainRepository;
|
||||
|
||||
}
|
||||
|
||||
@@ -49,6 +56,20 @@ public class SendVerificationEmailForRegistrationCommand : ISendVerificationEmai
|
||||
throw new ArgumentNullException(nameof(email));
|
||||
}
|
||||
|
||||
// Check if the email domain is blocked by an organization policy
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation))
|
||||
{
|
||||
var emailDomain = EmailValidation.GetDomain(email);
|
||||
|
||||
if (await _organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(emailDomain))
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"User registration email verification blocked by domain claim policy. Domain: {Domain}",
|
||||
emailDomain);
|
||||
throw new BadRequestException("This email address is claimed by an organization using Bitwarden.");
|
||||
}
|
||||
}
|
||||
|
||||
// Check to see if the user already exists
|
||||
var user = await _userRepository.GetByEmailAsync(email);
|
||||
var userExists = user != null;
|
||||
|
||||
25
src/Core/Billing/Models/SponsoredPlans.cs
Normal file
25
src/Core/Billing/Models/SponsoredPlans.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public class SponsoredPlans
|
||||
{
|
||||
public static IEnumerable<SponsoredPlan> All { get; set; } =
|
||||
[
|
||||
new()
|
||||
{
|
||||
PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise,
|
||||
SponsoredProductTierType = ProductTierType.Families,
|
||||
SponsoringProductTierType = ProductTierType.Enterprise,
|
||||
StripePlanId = "2021-family-for-enterprise-annually",
|
||||
UsersCanSponsor = org =>
|
||||
org.PlanType.GetProductTier() == ProductTierType.Enterprise,
|
||||
}
|
||||
];
|
||||
|
||||
public static SponsoredPlan Get(PlanSponsorshipType planSponsorshipType) =>
|
||||
All.FirstOrDefault(p => p.PlanSponsorshipType == planSponsorshipType)!;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
|
||||
public record CustomPlan : Plan
|
||||
{
|
||||
public CustomPlan()
|
||||
{
|
||||
Type = PlanType.Custom;
|
||||
PasswordManager = new CustomPasswordManagerFeatures();
|
||||
}
|
||||
|
||||
private record CustomPasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public CustomPasswordManagerFeatures()
|
||||
{
|
||||
AllowSeatAutoscale = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
|
||||
public record Enterprise2019Plan : Plan
|
||||
{
|
||||
public Enterprise2019Plan(bool isAnnual)
|
||||
{
|
||||
Type = isAnnual ? PlanType.EnterpriseAnnually2019 : PlanType.EnterpriseMonthly2019;
|
||||
ProductTier = ProductTierType.Enterprise;
|
||||
Name = isAnnual ? "Enterprise (Annually) 2019" : "Enterprise (Monthly) 2019";
|
||||
IsAnnual = isAnnual;
|
||||
NameLocalizationKey = "planNameEnterprise";
|
||||
DescriptionLocalizationKey = "planDescEnterprise";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasPolicies = true;
|
||||
HasSelfHost = true;
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
HasSso = true;
|
||||
HasOrganizationDomains = true;
|
||||
HasKeyConnector = true;
|
||||
HasScim = true;
|
||||
HasResetPassword = true;
|
||||
UsersGetPremium = true;
|
||||
HasCustomPermissions = true;
|
||||
|
||||
UpgradeSortOrder = 4;
|
||||
DisplaySortOrder = 4;
|
||||
LegacyYear = 2020;
|
||||
|
||||
SecretsManager = new Enterprise2019SecretsManagerFeatures(isAnnual);
|
||||
PasswordManager = new Enterprise2019PasswordManagerFeatures(isAnnual);
|
||||
}
|
||||
|
||||
private record Enterprise2019SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public Enterprise2019SecretsManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 200;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||
SeatPrice = 144;
|
||||
AdditionalPricePerServiceAccount = 6;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||
SeatPrice = 13;
|
||||
AdditionalPricePerServiceAccount = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Enterprise2019PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public Enterprise2019PasswordManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BaseStorageGb = 1;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
HasAdditionalSeatsOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "enterprise-org-seat-annually";
|
||||
SeatPrice = 36;
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "enterprise-org-seat-monthly";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
SeatPrice = 4M;
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
|
||||
public record Enterprise2020Plan : Plan
|
||||
{
|
||||
public Enterprise2020Plan(bool isAnnual)
|
||||
{
|
||||
Type = isAnnual ? PlanType.EnterpriseAnnually2020 : PlanType.EnterpriseMonthly2020;
|
||||
ProductTier = ProductTierType.Enterprise;
|
||||
Name = isAnnual ? "Enterprise (Annually) 2020" : "Enterprise (Monthly) 2020";
|
||||
IsAnnual = isAnnual;
|
||||
NameLocalizationKey = "planNameEnterprise";
|
||||
DescriptionLocalizationKey = "planDescEnterprise";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasPolicies = true;
|
||||
HasSelfHost = true;
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
HasSso = true;
|
||||
HasOrganizationDomains = true;
|
||||
HasKeyConnector = true;
|
||||
HasScim = true;
|
||||
HasResetPassword = true;
|
||||
UsersGetPremium = true;
|
||||
HasCustomPermissions = true;
|
||||
|
||||
UpgradeSortOrder = 4;
|
||||
DisplaySortOrder = 4;
|
||||
LegacyYear = 2023;
|
||||
|
||||
PasswordManager = new Enterprise2020PasswordManagerFeatures(isAnnual);
|
||||
SecretsManager = new Enterprise2020SecretsManagerFeatures(isAnnual);
|
||||
}
|
||||
|
||||
private record Enterprise2020SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public Enterprise2020SecretsManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 200;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||
SeatPrice = 144;
|
||||
AdditionalPricePerServiceAccount = 6;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||
SeatPrice = 13;
|
||||
AdditionalPricePerServiceAccount = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Enterprise2020PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public Enterprise2020PasswordManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BaseStorageGb = 1;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
HasAdditionalSeatsOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "2020-enterprise-org-seat-annually";
|
||||
SeatPrice = 60;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "2020-enterprise-seat-monthly";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
SeatPrice = 6;
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
|
||||
public record EnterprisePlan : Plan
|
||||
{
|
||||
public EnterprisePlan(bool isAnnual)
|
||||
{
|
||||
Type = isAnnual ? PlanType.EnterpriseAnnually : PlanType.EnterpriseMonthly;
|
||||
ProductTier = ProductTierType.Enterprise;
|
||||
Name = isAnnual ? "Enterprise (Annually)" : "Enterprise (Monthly)";
|
||||
IsAnnual = isAnnual;
|
||||
NameLocalizationKey = "planNameEnterprise";
|
||||
DescriptionLocalizationKey = "planDescEnterprise";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasPolicies = true;
|
||||
HasSelfHost = true;
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
HasSso = true;
|
||||
HasOrganizationDomains = true;
|
||||
HasKeyConnector = true;
|
||||
HasScim = true;
|
||||
HasResetPassword = true;
|
||||
UsersGetPremium = true;
|
||||
HasCustomPermissions = true;
|
||||
|
||||
UpgradeSortOrder = 4;
|
||||
DisplaySortOrder = 4;
|
||||
|
||||
PasswordManager = new EnterprisePasswordManagerFeatures(isAnnual);
|
||||
SecretsManager = new EnterpriseSecretsManagerFeatures(isAnnual);
|
||||
}
|
||||
|
||||
private record EnterpriseSecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public EnterpriseSecretsManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 50;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-annually";
|
||||
SeatPrice = 144;
|
||||
AdditionalPricePerServiceAccount = 12;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-monthly";
|
||||
SeatPrice = 13;
|
||||
AdditionalPricePerServiceAccount = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record EnterprisePasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public EnterprisePasswordManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BaseStorageGb = 1;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
HasAdditionalSeatsOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "2023-enterprise-org-seat-annually";
|
||||
StripeProviderPortalSeatPlanId = "password-manager-provider-portal-enterprise-annually-2024";
|
||||
SeatPrice = 72;
|
||||
ProviderPortalSeatPrice = 72;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "2023-enterprise-seat-monthly";
|
||||
StripeProviderPortalSeatPlanId = "password-manager-provider-portal-enterprise-monthly-2024";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
SeatPrice = 7;
|
||||
ProviderPortalSeatPrice = 6;
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
|
||||
public record Enterprise2023Plan : Plan
|
||||
{
|
||||
public Enterprise2023Plan(bool isAnnual)
|
||||
{
|
||||
Type = isAnnual ? PlanType.EnterpriseAnnually2023 : PlanType.EnterpriseMonthly2023;
|
||||
ProductTier = ProductTierType.Enterprise;
|
||||
Name = isAnnual ? "Enterprise (Annually)" : "Enterprise (Monthly)";
|
||||
IsAnnual = isAnnual;
|
||||
NameLocalizationKey = "planNameEnterprise";
|
||||
DescriptionLocalizationKey = "planDescEnterprise";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasPolicies = true;
|
||||
HasSelfHost = true;
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
HasSso = true;
|
||||
HasOrganizationDomains = true;
|
||||
HasKeyConnector = true;
|
||||
HasScim = true;
|
||||
HasResetPassword = true;
|
||||
UsersGetPremium = true;
|
||||
HasCustomPermissions = true;
|
||||
|
||||
UpgradeSortOrder = 4;
|
||||
DisplaySortOrder = 4;
|
||||
|
||||
LegacyYear = 2024;
|
||||
|
||||
PasswordManager = new Enterprise2023PasswordManagerFeatures(isAnnual);
|
||||
SecretsManager = new Enterprise2023SecretsManagerFeatures(isAnnual);
|
||||
}
|
||||
|
||||
private record Enterprise2023SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public Enterprise2023SecretsManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 200;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||
SeatPrice = 144;
|
||||
AdditionalPricePerServiceAccount = 6;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||
SeatPrice = 13;
|
||||
AdditionalPricePerServiceAccount = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Enterprise2023PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public Enterprise2023PasswordManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BaseStorageGb = 1;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
HasAdditionalSeatsOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "2023-enterprise-org-seat-annually";
|
||||
SeatPrice = 72;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "2023-enterprise-seat-monthly";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
SeatPrice = 7;
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
|
||||
public record Families2019Plan : Plan
|
||||
{
|
||||
public Families2019Plan()
|
||||
{
|
||||
Type = PlanType.FamiliesAnnually2019;
|
||||
ProductTier = ProductTierType.Families;
|
||||
Name = "Families 2019";
|
||||
IsAnnual = true;
|
||||
NameLocalizationKey = "planNameFamilies";
|
||||
DescriptionLocalizationKey = "planDescFamilies";
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasSelfHost = true;
|
||||
HasTotp = true;
|
||||
|
||||
UpgradeSortOrder = 1;
|
||||
DisplaySortOrder = 1;
|
||||
LegacyYear = 2020;
|
||||
|
||||
PasswordManager = new Families2019PasswordManagerFeatures();
|
||||
}
|
||||
|
||||
private record Families2019PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public Families2019PasswordManagerFeatures()
|
||||
{
|
||||
BaseSeats = 5;
|
||||
BaseStorageGb = 1;
|
||||
MaxSeats = 5;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
HasPremiumAccessOption = true;
|
||||
|
||||
StripePlanId = "personal-org-annually";
|
||||
StripeStoragePlanId = "personal-storage-gb-annually";
|
||||
StripePremiumAccessPlanId = "personal-org-premium-access-annually";
|
||||
BasePrice = 12;
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
PremiumAccessOptionPrice = 40;
|
||||
|
||||
AllowSeatAutoscale = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
|
||||
public record Families2025Plan : Plan
|
||||
{
|
||||
public Families2025Plan()
|
||||
{
|
||||
Type = PlanType.FamiliesAnnually2025;
|
||||
ProductTier = ProductTierType.Families;
|
||||
Name = "Families 2025";
|
||||
IsAnnual = true;
|
||||
NameLocalizationKey = "planNameFamilies";
|
||||
DescriptionLocalizationKey = "planDescFamilies";
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasSelfHost = true;
|
||||
HasTotp = true;
|
||||
UsersGetPremium = true;
|
||||
|
||||
UpgradeSortOrder = 1;
|
||||
DisplaySortOrder = 1;
|
||||
|
||||
PasswordManager = new Families2025PasswordManagerFeatures();
|
||||
}
|
||||
|
||||
private record Families2025PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public Families2025PasswordManagerFeatures()
|
||||
{
|
||||
BaseSeats = 6;
|
||||
BaseStorageGb = 1;
|
||||
MaxSeats = 6;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
|
||||
StripePlanId = "2020-families-org-annually";
|
||||
StripeStoragePlanId = "personal-storage-gb-annually";
|
||||
BasePrice = 40;
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
|
||||
AllowSeatAutoscale = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
|
||||
public record FamiliesPlan : Plan
|
||||
{
|
||||
public FamiliesPlan()
|
||||
{
|
||||
Type = PlanType.FamiliesAnnually;
|
||||
ProductTier = ProductTierType.Families;
|
||||
Name = "Families";
|
||||
IsAnnual = true;
|
||||
NameLocalizationKey = "planNameFamilies";
|
||||
DescriptionLocalizationKey = "planDescFamilies";
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasSelfHost = true;
|
||||
HasTotp = true;
|
||||
UsersGetPremium = true;
|
||||
|
||||
UpgradeSortOrder = 1;
|
||||
DisplaySortOrder = 1;
|
||||
|
||||
PasswordManager = new FamiliesPasswordManagerFeatures();
|
||||
}
|
||||
|
||||
private record FamiliesPasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public FamiliesPasswordManagerFeatures()
|
||||
{
|
||||
BaseSeats = 6;
|
||||
BaseStorageGb = 1;
|
||||
MaxSeats = 6;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
|
||||
StripePlanId = "2020-families-org-annually";
|
||||
StripeStoragePlanId = "personal-storage-gb-annually";
|
||||
BasePrice = 40;
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
|
||||
AllowSeatAutoscale = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
|
||||
public record FreePlan : Plan
|
||||
{
|
||||
public FreePlan()
|
||||
{
|
||||
Type = PlanType.Free;
|
||||
ProductTier = ProductTierType.Free;
|
||||
Name = "Free";
|
||||
NameLocalizationKey = "planNameFree";
|
||||
DescriptionLocalizationKey = "planDescFree";
|
||||
|
||||
UpgradeSortOrder = -1; // Always the lowest plan, cannot be upgraded to
|
||||
DisplaySortOrder = -1;
|
||||
|
||||
PasswordManager = new FreePasswordManagerFeatures();
|
||||
SecretsManager = new FreeSecretsManagerFeatures();
|
||||
}
|
||||
|
||||
private record FreeSecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public FreeSecretsManagerFeatures()
|
||||
{
|
||||
BaseSeats = 2;
|
||||
BaseServiceAccount = 3;
|
||||
MaxProjects = 3;
|
||||
MaxSeats = 2;
|
||||
MaxServiceAccounts = 3;
|
||||
|
||||
AllowSeatAutoscale = false;
|
||||
}
|
||||
}
|
||||
|
||||
private record FreePasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public FreePasswordManagerFeatures()
|
||||
{
|
||||
BaseSeats = 2;
|
||||
MaxCollections = 2;
|
||||
MaxSeats = 2;
|
||||
|
||||
AllowSeatAutoscale = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
|
||||
public record Teams2019Plan : Plan
|
||||
{
|
||||
public Teams2019Plan(bool isAnnual)
|
||||
{
|
||||
Type = isAnnual ? PlanType.TeamsAnnually2019 : PlanType.TeamsMonthly2019;
|
||||
ProductTier = ProductTierType.Teams;
|
||||
Name = isAnnual ? "Teams (Annually) 2019" : "Teams (Monthly) 2019";
|
||||
IsAnnual = isAnnual;
|
||||
NameLocalizationKey = "planNameTeams";
|
||||
DescriptionLocalizationKey = "planDescTeams";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
UsersGetPremium = true;
|
||||
|
||||
UpgradeSortOrder = 3;
|
||||
DisplaySortOrder = 3;
|
||||
LegacyYear = 2020;
|
||||
|
||||
SecretsManager = new Teams2019SecretsManagerFeatures(isAnnual);
|
||||
PasswordManager = new Teams2019PasswordManagerFeatures(isAnnual);
|
||||
}
|
||||
|
||||
private record Teams2019SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public Teams2019SecretsManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 50;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||
SeatPrice = 72;
|
||||
AdditionalPricePerServiceAccount = 6;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||
SeatPrice = 7;
|
||||
AdditionalPricePerServiceAccount = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Teams2019PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public Teams2019PasswordManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 5;
|
||||
BaseStorageGb = 1;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
HasAdditionalSeatsOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripePlanId = "teams-org-annually";
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "teams-org-seat-annually";
|
||||
SeatPrice = 24;
|
||||
BasePrice = 60;
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripePlanId = "teams-org-monthly";
|
||||
StripeSeatPlanId = "teams-org-seat-monthly";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
BasePrice = 8;
|
||||
SeatPrice = 2.5M;
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
|
||||
public record Teams2020Plan : Plan
|
||||
{
|
||||
public Teams2020Plan(bool isAnnual)
|
||||
{
|
||||
Type = isAnnual ? PlanType.TeamsAnnually2020 : PlanType.TeamsMonthly2020;
|
||||
ProductTier = ProductTierType.Teams;
|
||||
Name = isAnnual ? "Teams (Annually) 2020" : "Teams (Monthly) 2020";
|
||||
IsAnnual = isAnnual;
|
||||
NameLocalizationKey = "planNameTeams";
|
||||
DescriptionLocalizationKey = "planDescTeams";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
UsersGetPremium = true;
|
||||
|
||||
UpgradeSortOrder = 3;
|
||||
DisplaySortOrder = 3;
|
||||
LegacyYear = 2023;
|
||||
|
||||
PasswordManager = new Teams2020PasswordManagerFeatures(isAnnual);
|
||||
SecretsManager = new Teams2020SecretsManagerFeatures(isAnnual);
|
||||
}
|
||||
|
||||
private record Teams2020SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public Teams2020SecretsManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 50;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||
SeatPrice = 72;
|
||||
AdditionalPricePerServiceAccount = 6;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||
SeatPrice = 7;
|
||||
AdditionalPricePerServiceAccount = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Teams2020PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public Teams2020PasswordManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BaseStorageGb = 1;
|
||||
BasePrice = 0;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
HasAdditionalSeatsOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "2020-teams-org-seat-annually";
|
||||
SeatPrice = 36;
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "2020-teams-org-seat-monthly";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
SeatPrice = 4;
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
|
||||
public record TeamsPlan : Plan
|
||||
{
|
||||
public TeamsPlan(bool isAnnual)
|
||||
{
|
||||
Type = isAnnual ? PlanType.TeamsAnnually : PlanType.TeamsMonthly;
|
||||
ProductTier = ProductTierType.Teams;
|
||||
Name = isAnnual ? "Teams (Annually)" : "Teams (Monthly)";
|
||||
IsAnnual = isAnnual;
|
||||
NameLocalizationKey = "planNameTeams";
|
||||
DescriptionLocalizationKey = "planDescTeams";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
UsersGetPremium = true;
|
||||
HasScim = true;
|
||||
|
||||
UpgradeSortOrder = 3;
|
||||
DisplaySortOrder = 3;
|
||||
|
||||
PasswordManager = new TeamsPasswordManagerFeatures(isAnnual);
|
||||
SecretsManager = new TeamsSecretsManagerFeatures(isAnnual);
|
||||
}
|
||||
|
||||
private record TeamsSecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public TeamsSecretsManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 20;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-annually";
|
||||
SeatPrice = 72;
|
||||
AdditionalPricePerServiceAccount = 12;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-monthly";
|
||||
SeatPrice = 7;
|
||||
AdditionalPricePerServiceAccount = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record TeamsPasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public TeamsPasswordManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BaseStorageGb = 1;
|
||||
BasePrice = 0;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
HasAdditionalSeatsOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "2023-teams-org-seat-annually";
|
||||
SeatPrice = 48;
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "2023-teams-org-seat-monthly";
|
||||
StripeProviderPortalSeatPlanId = "password-manager-provider-portal-teams-monthly-2024";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
SeatPrice = 5;
|
||||
ProviderPortalSeatPrice = 4;
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
|
||||
public record Teams2023Plan : Plan
|
||||
{
|
||||
public Teams2023Plan(bool isAnnual)
|
||||
{
|
||||
Type = isAnnual ? PlanType.TeamsAnnually2023 : PlanType.TeamsMonthly2023;
|
||||
ProductTier = ProductTierType.Teams;
|
||||
Name = isAnnual ? "Teams (Annually)" : "Teams (Monthly)";
|
||||
IsAnnual = isAnnual;
|
||||
NameLocalizationKey = "planNameTeams";
|
||||
DescriptionLocalizationKey = "planDescTeams";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
UsersGetPremium = true;
|
||||
|
||||
UpgradeSortOrder = 3;
|
||||
DisplaySortOrder = 3;
|
||||
|
||||
LegacyYear = 2024;
|
||||
|
||||
PasswordManager = new Teams2023PasswordManagerFeatures(isAnnual);
|
||||
SecretsManager = new Teams2023SecretsManagerFeatures(isAnnual);
|
||||
}
|
||||
|
||||
private record Teams2023SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public Teams2023SecretsManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 50;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||
SeatPrice = 72;
|
||||
AdditionalPricePerServiceAccount = 6;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||
SeatPrice = 7;
|
||||
AdditionalPricePerServiceAccount = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Teams2023PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public Teams2023PasswordManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BaseStorageGb = 1;
|
||||
BasePrice = 0;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
HasAdditionalSeatsOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "2023-teams-org-seat-annually";
|
||||
SeatPrice = 48;
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "2023-teams-org-seat-monthly";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
SeatPrice = 5;
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
|
||||
public record TeamsStarterPlan : Plan
|
||||
{
|
||||
public TeamsStarterPlan()
|
||||
{
|
||||
Type = PlanType.TeamsStarter;
|
||||
ProductTier = ProductTierType.TeamsStarter;
|
||||
Name = "Teams (Starter)";
|
||||
NameLocalizationKey = "planNameTeamsStarter";
|
||||
DescriptionLocalizationKey = "planDescTeams";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
UsersGetPremium = true;
|
||||
|
||||
UpgradeSortOrder = 2;
|
||||
DisplaySortOrder = 2;
|
||||
|
||||
PasswordManager = new TeamsStarterPasswordManagerFeatures();
|
||||
SecretsManager = new TeamsStarterSecretsManagerFeatures();
|
||||
|
||||
LegacyYear = 2024;
|
||||
}
|
||||
|
||||
private record TeamsStarterSecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public TeamsStarterSecretsManagerFeatures()
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 20;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-monthly";
|
||||
SeatPrice = 7;
|
||||
AdditionalPricePerServiceAccount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
private record TeamsStarterPasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public TeamsStarterPasswordManagerFeatures()
|
||||
{
|
||||
BaseSeats = 10;
|
||||
BaseStorageGb = 1;
|
||||
BasePrice = 20;
|
||||
|
||||
MaxSeats = 10;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
|
||||
StripePlanId = "teams-org-starter";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
|
||||
public record TeamsStarterPlan2023 : Plan
|
||||
{
|
||||
public TeamsStarterPlan2023()
|
||||
{
|
||||
Type = PlanType.TeamsStarter2023;
|
||||
ProductTier = ProductTierType.TeamsStarter;
|
||||
Name = "Teams (Starter)";
|
||||
NameLocalizationKey = "planNameTeamsStarter";
|
||||
DescriptionLocalizationKey = "planDescTeams";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
UsersGetPremium = true;
|
||||
|
||||
UpgradeSortOrder = 2;
|
||||
DisplaySortOrder = 2;
|
||||
|
||||
PasswordManager = new TeamsStarter2023PasswordManagerFeatures();
|
||||
SecretsManager = new TeamsStarter2023SecretsManagerFeatures();
|
||||
LegacyYear = 2024;
|
||||
}
|
||||
|
||||
private record TeamsStarter2023SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public TeamsStarter2023SecretsManagerFeatures()
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 50;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||
SeatPrice = 7;
|
||||
AdditionalPricePerServiceAccount = 0.5M;
|
||||
}
|
||||
}
|
||||
|
||||
private record TeamsStarter2023PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public TeamsStarter2023PasswordManagerFeatures()
|
||||
{
|
||||
BaseSeats = 10;
|
||||
BaseStorageGb = 1;
|
||||
BasePrice = 20;
|
||||
|
||||
MaxSeats = 10;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
|
||||
StripePlanId = "teams-org-starter";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,12 @@ using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OneOf;
|
||||
using Stripe;
|
||||
@@ -54,7 +54,7 @@ public class PreviewOrganizationTaxCommand(
|
||||
switch (purchase)
|
||||
{
|
||||
case { PasswordManager.Sponsored: true }:
|
||||
var sponsoredPlan = StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise);
|
||||
var sponsoredPlan = SponsoredPlans.Get(PlanSponsorshipType.FamiliesForEnterprise);
|
||||
items.Add(new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Price = sponsoredPlan.StripePlanId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Models.Business;
|
||||
using Stripe;
|
||||
|
||||
@@ -17,7 +18,7 @@ public class SponsorOrganizationSubscriptionUpdate : SubscriptionUpdate
|
||||
{
|
||||
_existingPlanStripeId = existingPlan.PasswordManager.StripePlanId;
|
||||
_sponsoredPlanStripeId = sponsoredPlan?.StripePlanId
|
||||
?? Core.Utilities.StaticStore.SponsoredPlans.FirstOrDefault()?.StripePlanId;
|
||||
?? SponsoredPlans.All.FirstOrDefault()?.StripePlanId;
|
||||
_applySponsorship = applySponsorship;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ using Bit.Core.Billing.Pricing.Organizations;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Billing.Pricing;
|
||||
@@ -28,13 +27,6 @@ public class PricingClient(
|
||||
return null;
|
||||
}
|
||||
|
||||
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
|
||||
|
||||
if (!usePricingService)
|
||||
{
|
||||
return StaticStore.GetPlan(planType);
|
||||
}
|
||||
|
||||
var lookupKey = GetLookupKey(planType);
|
||||
|
||||
if (lookupKey == null)
|
||||
@@ -77,13 +69,6 @@ public class PricingClient(
|
||||
return [];
|
||||
}
|
||||
|
||||
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
|
||||
|
||||
if (!usePricingService)
|
||||
{
|
||||
return StaticStore.Plans.ToList();
|
||||
}
|
||||
|
||||
var response = await httpClient.GetAsync("plans/organization");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
@@ -114,11 +99,10 @@ public class PricingClient(
|
||||
return [];
|
||||
}
|
||||
|
||||
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
|
||||
var fetchPremiumPriceFromPricingService =
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26793_FetchPremiumPriceFromPricingService);
|
||||
|
||||
if (!usePricingService || !fetchPremiumPriceFromPricingService)
|
||||
if (!fetchPremiumPriceFromPricingService)
|
||||
{
|
||||
return [CurrentPremiumPlan];
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ public static class FeatureFlagKeys
|
||||
public const string AutomaticConfirmUsers = "pm-19934-auto-confirm-organization-users";
|
||||
public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache";
|
||||
public const string AccountRecoveryCommand = "pm-25581-prevent-provider-account-recovery";
|
||||
public const string BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration";
|
||||
public const string PolicyValidatorsRefactor = "pm-26423-refactor-policy-side-effects";
|
||||
|
||||
/* Architecture */
|
||||
@@ -185,7 +186,6 @@ public static class FeatureFlagKeys
|
||||
|
||||
/* Billing Team */
|
||||
public const string TrialPayment = "PM-8163-trial-payment";
|
||||
public const string UsePricingService = "use-pricing-service";
|
||||
public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates";
|
||||
public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover";
|
||||
public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings";
|
||||
@@ -243,7 +243,6 @@ public static class FeatureFlagKeys
|
||||
|
||||
/* Vault Team */
|
||||
public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge";
|
||||
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
|
||||
public const string CipherKeyEncryption = "cipher-key-encryption";
|
||||
public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk";
|
||||
public const string EndUserNotifications = "pm-10609-end-user-notifications";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Stripe;
|
||||
|
||||
#nullable enable
|
||||
@@ -150,7 +151,7 @@ public class SubscriptionInfo
|
||||
}
|
||||
|
||||
Quantity = (int)item.Quantity;
|
||||
SponsoredSubscriptionItem = item.Plan != null && Utilities.StaticStore.SponsoredPlans.Any(p => p.StripePlanId == item.Plan.Id);
|
||||
SponsoredSubscriptionItem = item.Plan != null && SponsoredPlans.All.Any(p => p.StripePlanId == item.Plan.Id);
|
||||
}
|
||||
|
||||
public bool AddonSubscriptionItem { get; set; }
|
||||
|
||||
@@ -14,6 +14,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
@@ -135,6 +136,8 @@ public static class OrganizationServiceCollectionExtensions
|
||||
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
|
||||
services.AddScoped<IConfirmOrganizationUserCommand, ConfirmOrganizationUserCommand>();
|
||||
services.AddScoped<IAdminRecoverAccountCommand, AdminRecoverAccountCommand>();
|
||||
services.AddScoped<IAutomaticallyConfirmOrganizationUserCommand, AutomaticallyConfirmOrganizationUserCommand>();
|
||||
services.AddScoped<IAutomaticallyConfirmOrganizationUsersValidator, AutomaticallyConfirmOrganizationUsersValidator>();
|
||||
|
||||
services.AddScoped<IDeleteClaimedOrganizationUserAccountCommand, DeleteClaimedOrganizationUserAccountCommand>();
|
||||
services.AddScoped<IDeleteClaimedOrganizationUserAccountValidator, DeleteClaimedOrganizationUserAccountValidator>();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@@ -7,7 +8,6 @@ using Bit.Core.Models.Data.Organizations.OrganizationSponsorships;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;
|
||||
|
||||
@@ -54,10 +54,9 @@ public class CloudSyncSponsorshipsCommand : ICloudSyncSponsorshipsCommand
|
||||
|
||||
foreach (var selfHostedSponsorship in sponsorshipsData)
|
||||
{
|
||||
var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(selfHostedSponsorship.PlanSponsorshipType)?.SponsoringProductTierType;
|
||||
var requiredSponsoringProductType = SponsoredPlans.Get(selfHostedSponsorship.PlanSponsorshipType).SponsoringProductTierType;
|
||||
var sponsoringOrgProductTier = sponsoringOrg.PlanType.GetProductTier();
|
||||
if (requiredSponsoringProductType == null
|
||||
|| sponsoringOrgProductTier != requiredSponsoringProductType.Value)
|
||||
if (sponsoringOrgProductTier != requiredSponsoringProductType)
|
||||
{
|
||||
continue; // prevent unsupported sponsorships
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;
|
||||
|
||||
@@ -50,11 +50,10 @@ public class SetUpSponsorshipCommand : ISetUpSponsorshipCommand
|
||||
}
|
||||
|
||||
// Check org to sponsor's product type
|
||||
var requiredSponsoredProductType = StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value)?.SponsoredProductTierType;
|
||||
var requiredSponsoredProductType = SponsoredPlans.Get(sponsorship.PlanSponsorshipType.Value).SponsoredProductTierType;
|
||||
var sponsoredOrganizationProductTier = sponsoredOrganization.PlanType.GetProductTier();
|
||||
|
||||
if (requiredSponsoredProductType == null ||
|
||||
sponsoredOrganizationProductTier != requiredSponsoredProductType.Value)
|
||||
if (sponsoredOrganizationProductTier != requiredSponsoredProductType)
|
||||
{
|
||||
throw new BadRequestException("Can only redeem sponsorship offer on families organizations.");
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -95,7 +96,7 @@ public class ValidateSponsorshipCommand : CancelSponsorshipCommand, IValidateSpo
|
||||
return false;
|
||||
}
|
||||
|
||||
var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(existingSponsorship.PlanSponsorshipType.Value);
|
||||
var sponsoredPlan = SponsoredPlans.Get(existingSponsorship.PlanSponsorshipType.Value);
|
||||
|
||||
var sponsoringOrganization = await _organizationRepository
|
||||
.GetByIdAsync(existingSponsorship.SponsoringOrganizationId.Value);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -7,7 +8,6 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;
|
||||
|
||||
@@ -34,11 +34,10 @@ public class CreateSponsorshipCommand(
|
||||
throw new BadRequestException("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email.");
|
||||
}
|
||||
|
||||
var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(sponsorshipType)?.SponsoringProductTierType;
|
||||
var requiredSponsoringProductType = SponsoredPlans.Get(sponsorshipType).SponsoringProductTierType;
|
||||
var sponsoringOrgProductTier = sponsoringOrganization.PlanType.GetProductTier();
|
||||
|
||||
if (requiredSponsoringProductType == null ||
|
||||
sponsoringOrgProductTier != requiredSponsoringProductType.Value)
|
||||
if (sponsoringOrgProductTier != requiredSponsoringProductType)
|
||||
{
|
||||
throw new BadRequestException("Specified Organization cannot sponsor other organizations.");
|
||||
}
|
||||
|
||||
@@ -17,4 +17,5 @@ public interface IOrganizationDomainRepository : IRepository<OrganizationDomain,
|
||||
Task<OrganizationDomain?> GetDomainByOrgIdAndDomainNameAsync(Guid orgId, string domainName);
|
||||
Task<ICollection<OrganizationDomain>> GetExpiredOrganizationDomainsAsync();
|
||||
Task<bool> DeleteExpiredAsync(int expirationPeriod);
|
||||
Task<bool> HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(string domainName, Guid? excludeOrganizationId = null);
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
var existingPlan = await _pricingClient.GetPlanOrThrow(org.PlanType);
|
||||
var sponsoredPlan = sponsorship?.PlanSponsorshipType != null
|
||||
? Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value)
|
||||
? SponsoredPlans.Get(sponsorship.PlanSponsorshipType.Value)
|
||||
: null;
|
||||
var subscriptionUpdate =
|
||||
new SponsorOrganizationSubscriptionUpdate(existingPlan, sponsoredPlan, applySponsorship);
|
||||
@@ -1072,7 +1072,7 @@ public class StripePaymentService : IPaymentService
|
||||
|
||||
if (isSponsored)
|
||||
{
|
||||
var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(parameters.PasswordManager.SponsoredPlan.Value);
|
||||
var sponsoredPlan = SponsoredPlans.Get(parameters.PasswordManager.SponsoredPlan.Value);
|
||||
options.SubscriptionDetails.Items.Add(
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = sponsoredPlan.StripePlanId }
|
||||
);
|
||||
|
||||
@@ -21,7 +21,7 @@ public class GlobalSettings : IGlobalSettings
|
||||
}
|
||||
|
||||
public bool SelfHosted { get; set; }
|
||||
public bool UnifiedDeployment { get; set; }
|
||||
public bool LiteDeployment { get; set; }
|
||||
public virtual string KnownProxies { get; set; }
|
||||
public virtual string SiteName { get; set; }
|
||||
public virtual string ProjectName { get; set; }
|
||||
@@ -783,7 +783,18 @@ public class GlobalSettings : IGlobalSettings
|
||||
{
|
||||
public virtual IConnectionStringSettings Redis { get; set; } = new ConnectionStringSettings();
|
||||
public virtual IConnectionStringSettings Cosmos { get; set; } = new ConnectionStringSettings();
|
||||
public ExtendedCacheSettings DefaultExtendedCache { get; set; } = new ExtendedCacheSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A collection of Settings for customizing the FusionCache used in extended caching. Defaults are
|
||||
/// provided for every attribute so that only specific values need to be overridden if needed.
|
||||
/// </summary>
|
||||
public class ExtendedCacheSettings
|
||||
{
|
||||
public bool EnableDistributedCache { get; set; } = true;
|
||||
public bool UseSharedRedisCache { get; set; } = true;
|
||||
public IConnectionStringSettings Redis { get; set; } = new ConnectionStringSettings();
|
||||
public TimeSpan Duration { get; set; } = TimeSpan.FromMinutes(30);
|
||||
public bool IsFailSafeEnabled { get; set; } = true;
|
||||
public TimeSpan FailSafeMaxDuration { get; set; } = TimeSpan.FromHours(2);
|
||||
|
||||
@@ -6,7 +6,7 @@ public interface IGlobalSettings
|
||||
{
|
||||
// This interface exists for testing. Add settings here as needed for testing
|
||||
bool SelfHosted { get; set; }
|
||||
bool UnifiedDeployment { get; set; }
|
||||
bool LiteDeployment { get; set; }
|
||||
string KnownProxies { get; set; }
|
||||
string ProjectName { get; set; }
|
||||
bool EnableCloudCommunication { get; set; }
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Net.Mail;
|
||||
using System.Text.RegularExpressions;
|
||||
using Bit.Core.Exceptions;
|
||||
using MimeKit;
|
||||
|
||||
namespace Bit.Core.Utilities;
|
||||
@@ -41,4 +43,22 @@ public static class EmailValidation
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the domain portion from an email address and normalizes it to lowercase.
|
||||
/// </summary>
|
||||
/// <param name="email">The email address to extract the domain from.</param>
|
||||
/// <returns>The domain portion of the email address in lowercase (e.g., "example.com").</returns>
|
||||
/// <exception cref="BadRequestException">Thrown when the email address format is invalid.</exception>
|
||||
public static string GetDomain(string email)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new MailAddress(email).Host.ToLower();
|
||||
}
|
||||
catch (Exception ex) when (ex is FormatException || ex is ArgumentException)
|
||||
{
|
||||
throw new BadRequestException("Invalid email address format.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Caching.StackExchangeRedis;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StackExchange.Redis;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
using ZiggyCreatures.Caching.Fusion.Backplane;
|
||||
@@ -14,77 +15,149 @@ namespace Microsoft.Extensions.DependencyInjection;
|
||||
public static class ExtendedCacheServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Add Fusion Cache <see href="https://github.com/ZiggyCreatures/FusionCache"/> to the service
|
||||
/// collection.<br/>
|
||||
/// Adds a new, named Fusion Cache <see href="https://github.com/ZiggyCreatures/FusionCache"/> to the service
|
||||
/// collection. If an existing cache of the same name is found, it will do nothing.<br/>
|
||||
/// <br/>
|
||||
/// If Redis is configured, it uses Redis for an L2 cache and backplane. If not, it simply uses in-memory caching.
|
||||
/// <b>Note</b>: When re-using the existing Redis cache, it is expected to call this method <b>after</b> calling
|
||||
/// <code>services.AddDistributedCache(globalSettings)</code><br />This ensures that DI correctly finds,
|
||||
/// configures, and re-uses all the shared Redis architecture.
|
||||
/// </summary>
|
||||
public static IServiceCollection TryAddExtendedCacheServices(this IServiceCollection services, GlobalSettings globalSettings)
|
||||
public static IServiceCollection AddExtendedCache(
|
||||
this IServiceCollection services,
|
||||
string cacheName,
|
||||
GlobalSettings globalSettings,
|
||||
GlobalSettings.ExtendedCacheSettings? settings = null)
|
||||
{
|
||||
if (services.Any(s => s.ServiceType == typeof(IFusionCache)))
|
||||
settings ??= globalSettings.DistributedCache.DefaultExtendedCache;
|
||||
if (settings is null || string.IsNullOrEmpty(cacheName))
|
||||
{
|
||||
return services;
|
||||
}
|
||||
|
||||
var fusionCacheBuilder = services.AddFusionCache()
|
||||
.WithOptions(options =>
|
||||
// If a cache already exists with this key, do nothing
|
||||
if (services.Any(s => s.ServiceType == typeof(IFusionCache) &&
|
||||
s.ServiceKey?.Equals(cacheName) == true))
|
||||
{
|
||||
return services;
|
||||
}
|
||||
|
||||
if (services.All(s => s.ServiceType != typeof(FusionCacheSystemTextJsonSerializer)))
|
||||
{
|
||||
services.AddFusionCacheSystemTextJsonSerializer();
|
||||
}
|
||||
var fusionCacheBuilder = services
|
||||
.AddFusionCache(cacheName)
|
||||
.WithCacheKeyPrefix($"{cacheName}:")
|
||||
.AsKeyedServiceByCacheName()
|
||||
.WithOptions(opt =>
|
||||
{
|
||||
options.DistributedCacheCircuitBreakerDuration = globalSettings.DistributedCache.DistributedCacheCircuitBreakerDuration;
|
||||
opt.DistributedCacheCircuitBreakerDuration = settings.DistributedCacheCircuitBreakerDuration;
|
||||
})
|
||||
.WithDefaultEntryOptions(new FusionCacheEntryOptions
|
||||
{
|
||||
Duration = globalSettings.DistributedCache.Duration,
|
||||
IsFailSafeEnabled = globalSettings.DistributedCache.IsFailSafeEnabled,
|
||||
FailSafeMaxDuration = globalSettings.DistributedCache.FailSafeMaxDuration,
|
||||
FailSafeThrottleDuration = globalSettings.DistributedCache.FailSafeThrottleDuration,
|
||||
EagerRefreshThreshold = globalSettings.DistributedCache.EagerRefreshThreshold,
|
||||
FactorySoftTimeout = globalSettings.DistributedCache.FactorySoftTimeout,
|
||||
FactoryHardTimeout = globalSettings.DistributedCache.FactoryHardTimeout,
|
||||
DistributedCacheSoftTimeout = globalSettings.DistributedCache.DistributedCacheSoftTimeout,
|
||||
DistributedCacheHardTimeout = globalSettings.DistributedCache.DistributedCacheHardTimeout,
|
||||
AllowBackgroundDistributedCacheOperations = globalSettings.DistributedCache.AllowBackgroundDistributedCacheOperations,
|
||||
JitterMaxDuration = globalSettings.DistributedCache.JitterMaxDuration
|
||||
Duration = settings.Duration,
|
||||
IsFailSafeEnabled = settings.IsFailSafeEnabled,
|
||||
FailSafeMaxDuration = settings.FailSafeMaxDuration,
|
||||
FailSafeThrottleDuration = settings.FailSafeThrottleDuration,
|
||||
EagerRefreshThreshold = settings.EagerRefreshThreshold,
|
||||
FactorySoftTimeout = settings.FactorySoftTimeout,
|
||||
FactoryHardTimeout = settings.FactoryHardTimeout,
|
||||
DistributedCacheSoftTimeout = settings.DistributedCacheSoftTimeout,
|
||||
DistributedCacheHardTimeout = settings.DistributedCacheHardTimeout,
|
||||
AllowBackgroundDistributedCacheOperations = settings.AllowBackgroundDistributedCacheOperations,
|
||||
JitterMaxDuration = settings.JitterMaxDuration
|
||||
})
|
||||
.WithSerializer(
|
||||
new FusionCacheSystemTextJsonSerializer()
|
||||
);
|
||||
.WithRegisteredSerializer();
|
||||
|
||||
if (!CoreHelpers.SettingHasValue(globalSettings.DistributedCache.Redis.ConnectionString))
|
||||
{
|
||||
if (!settings.EnableDistributedCache)
|
||||
return services;
|
||||
}
|
||||
|
||||
services.TryAddSingleton<IConnectionMultiplexer>(sp =>
|
||||
ConnectionMultiplexer.Connect(globalSettings.DistributedCache.Redis.ConnectionString));
|
||||
if (settings.UseSharedRedisCache)
|
||||
{
|
||||
// Using Shared Redis, TryAdd and reuse all pieces (multiplexer, distributed cache and backplane)
|
||||
|
||||
fusionCacheBuilder
|
||||
.WithDistributedCache(sp =>
|
||||
if (!CoreHelpers.SettingHasValue(globalSettings.DistributedCache.Redis.ConnectionString))
|
||||
return services;
|
||||
|
||||
services.TryAddSingleton<IConnectionMultiplexer>(sp =>
|
||||
CreateConnectionMultiplexer(sp, cacheName, globalSettings.DistributedCache.Redis.ConnectionString));
|
||||
|
||||
services.TryAddSingleton<IDistributedCache>(sp =>
|
||||
{
|
||||
var cache = sp.GetService<IDistributedCache>();
|
||||
if (cache is not null)
|
||||
{
|
||||
return cache;
|
||||
}
|
||||
var mux = sp.GetRequiredService<IConnectionMultiplexer>();
|
||||
return new RedisCache(new RedisCacheOptions
|
||||
{
|
||||
ConnectionMultiplexerFactory = () => Task.FromResult(mux)
|
||||
});
|
||||
})
|
||||
.WithBackplane(sp =>
|
||||
{
|
||||
var backplane = sp.GetService<IFusionCacheBackplane>();
|
||||
if (backplane is not null)
|
||||
});
|
||||
|
||||
services.TryAddSingleton<IFusionCacheBackplane>(sp =>
|
||||
{
|
||||
return backplane;
|
||||
}
|
||||
var mux = sp.GetRequiredService<IConnectionMultiplexer>();
|
||||
var mux = sp.GetRequiredService<IConnectionMultiplexer>();
|
||||
return new RedisBackplane(new RedisBackplaneOptions
|
||||
{
|
||||
ConnectionMultiplexerFactory = () => Task.FromResult(mux)
|
||||
});
|
||||
});
|
||||
|
||||
fusionCacheBuilder
|
||||
.WithRegisteredDistributedCache()
|
||||
.WithRegisteredBackplane();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
// Using keyed Redis / Distributed Cache. Create all pieces as keyed services.
|
||||
|
||||
if (!CoreHelpers.SettingHasValue(settings.Redis.ConnectionString))
|
||||
return services;
|
||||
|
||||
services.TryAddKeyedSingleton<IConnectionMultiplexer>(
|
||||
cacheName,
|
||||
(sp, _) => CreateConnectionMultiplexer(sp, cacheName, settings.Redis.ConnectionString)
|
||||
);
|
||||
services.TryAddKeyedSingleton<IDistributedCache>(
|
||||
cacheName,
|
||||
(sp, _) =>
|
||||
{
|
||||
var mux = sp.GetRequiredKeyedService<IConnectionMultiplexer>(cacheName);
|
||||
return new RedisCache(new RedisCacheOptions
|
||||
{
|
||||
ConnectionMultiplexerFactory = () => Task.FromResult(mux)
|
||||
});
|
||||
}
|
||||
);
|
||||
services.TryAddKeyedSingleton<IFusionCacheBackplane>(
|
||||
cacheName,
|
||||
(sp, _) =>
|
||||
{
|
||||
var mux = sp.GetRequiredKeyedService<IConnectionMultiplexer>(cacheName);
|
||||
return new RedisBackplane(new RedisBackplaneOptions
|
||||
{
|
||||
ConnectionMultiplexerFactory = () => Task.FromResult(mux)
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
fusionCacheBuilder
|
||||
.WithRegisteredKeyedDistributedCacheByCacheName()
|
||||
.WithRegisteredKeyedBackplaneByCacheName();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static ConnectionMultiplexer CreateConnectionMultiplexer(IServiceProvider sp, string cacheName,
|
||||
string connectionString)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ConnectionMultiplexer.Connect(connectionString);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var logger = sp.GetService<ILogger>();
|
||||
logger?.LogError(ex, "Failed to connect to Redis for cache {CacheName}", cacheName);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
157
src/Core/Utilities/README.md
Normal file
157
src/Core/Utilities/README.md
Normal file
@@ -0,0 +1,157 @@
|
||||
## Extended Cache
|
||||
|
||||
`ExtendedCache` is a wrapper around [FusionCache](https://github.com/ZiggyCreatures/FusionCache)
|
||||
that provides a simple way to register **named, isolated caches** with sensible defaults.
|
||||
The goal is to make it trivial for each subsystem or feature to have its own cache -
|
||||
with optional distributed caching and backplane support - without repeatedly wiring up
|
||||
FusionCache, Redis, and related infrastructure.
|
||||
|
||||
Each named cache automatically receives:
|
||||
|
||||
- Its own `FusionCache` instance
|
||||
- Its own configuration (default or overridden)
|
||||
- Its own key prefix
|
||||
- Optional distributed store
|
||||
- Optional backplane
|
||||
|
||||
`ExtendedCache` supports several deployment modes:
|
||||
|
||||
- **Memory-only caching** (with stampede protection)
|
||||
- **Memory + distributed cache + backplane** using the **shared** application Redis
|
||||
- **Memory + distributed cache + backplane** using a **fully isolated** Redis instance
|
||||
|
||||
**Note**: When using the shared Redis cache option (which is on by default, if the
|
||||
Redis connection string is configured), it is expected to call
|
||||
`services.AddDistributedCache(globalSettings)` **before** calling
|
||||
`AddExtendedCache`. The idea is to set up the distributed cache in our normal pattern
|
||||
and then "extend" it to include more functionality.
|
||||
|
||||
### Configuration
|
||||
|
||||
`ExtendedCache` exposes a set of default properties that define how each named cache behaves.
|
||||
These map directly to FusionCache configuration options such as timeouts, duration,
|
||||
jitter, fail-safe mode, etc. Any cache can override these defaults independently.
|
||||
|
||||
#### Default configuration
|
||||
|
||||
The simplest approach registers a new named cache with default settings and reusing
|
||||
the existing distributed cache:
|
||||
|
||||
``` csharp
|
||||
services.AddDistributedCache(globalSettings);
|
||||
services.AddExtendedCache(cacheName, globalSettings);
|
||||
```
|
||||
|
||||
By default:
|
||||
- If `GlobalSettings.DistributedCache.Redis.ConnectionString` is configured:
|
||||
- The cache is memory + distributed (Redis)
|
||||
- The Redis cache created by `AddDistributedCache` is re-used
|
||||
- A Redis backplane is configured, re-using the same multiplexer
|
||||
- If Redis is **not** configured the cache automatically falls back to memory-only
|
||||
|
||||
#### Overriding default properties
|
||||
|
||||
A number of default properties are provided (see
|
||||
`GlobalSettings.DistributedCache.DefaultExtendedCache` for specific values). A named
|
||||
cache can override any (or all) of these properties simply by providing its own
|
||||
instance of `ExtendedCacheSettings`:
|
||||
|
||||
``` csharp
|
||||
services.AddExtendedCache(cacheName, globalSettings, new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
Duration = TimeSpan.FromHours(1),
|
||||
});
|
||||
```
|
||||
|
||||
This example keeps all other defaults—including shared Redis—but changes the
|
||||
default cached item duration from 30 minutes to 1 hour.
|
||||
|
||||
#### Isolated Redis configuration
|
||||
|
||||
ExtendedCache can also run in a fully isolated mode where the cache uses its own:
|
||||
- Redis multiplexer
|
||||
- Distributed cache
|
||||
- Backplane
|
||||
|
||||
To enable this, specify a Redis connection string and set `UseSharedRedisCache`
|
||||
to `false`:
|
||||
|
||||
``` csharp
|
||||
services.AddExtendedCache(cacheName, globalSettings, new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
UseSharedRedisCache = false,
|
||||
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" }
|
||||
});
|
||||
```
|
||||
|
||||
When configured this way:
|
||||
- A dedicated `IConnectionMultiplexer` is created
|
||||
- A dedicated `IDistributedCache` is created
|
||||
- A dedicated FusionCache backplane is created
|
||||
- All three are exposed to DI as keyed services (using the cache name as service key)
|
||||
|
||||
### Accessing a named cache
|
||||
|
||||
A named cache can be retrieved either:
|
||||
- Directly via DI using keyed services
|
||||
- Through `IFusionCacheProvider` (similar to
|
||||
[IHttpClientFactory](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-7.0#named-clients))
|
||||
|
||||
#### Keyed service
|
||||
|
||||
In the consuming class, declare an IFusionCache field:
|
||||
|
||||
```csharp
|
||||
private IFusionCache _cache;
|
||||
```
|
||||
|
||||
Then ask DI to inject the keyed cache:
|
||||
|
||||
```csharp
|
||||
public MyService([FromKeyedServices("MyCache")] IFusionCache cache)
|
||||
{
|
||||
_cache = cache;
|
||||
}
|
||||
```
|
||||
|
||||
Or request it manually:
|
||||
|
||||
```csharp
|
||||
cache: provider.GetRequiredKeyedService<IFusionCache>(serviceKey: cacheName)
|
||||
```
|
||||
|
||||
#### Injecting a provider
|
||||
|
||||
Alternatively, an `IFusionCacheProvider` can be injected and used to request a named
|
||||
cache - similar to how `IHttpClientFactory` can be used to create named `HttpClient`
|
||||
instances
|
||||
|
||||
In the class using the cache, use an injected provider to request the named cache:
|
||||
|
||||
```csharp
|
||||
private readonly IFusionCache _cache;
|
||||
|
||||
public MyController(IFusionCacheProvider cacheProvider)
|
||||
{
|
||||
_cache = cacheProvider.GetCache("CacheName");
|
||||
}
|
||||
```
|
||||
|
||||
### Using a cache
|
||||
|
||||
Using the cache in code is as simple as replacing the direct repository calls with
|
||||
`FusionCache`'s `GetOrSet` call. If the class previously fetched an `Item` from
|
||||
an `ItemRepository`, all that we need to do is provide a key and the original
|
||||
repository call as the fallback:
|
||||
|
||||
```csharp
|
||||
var item = _cache.GetOrSet<Item>(
|
||||
$"item:{id}",
|
||||
_ => _itemRepository.GetById(id)
|
||||
);
|
||||
```
|
||||
|
||||
`ExtendedCache` doesn’t change how `FusionCache` is used in code, which means all
|
||||
the functionality and full `FusionCache` API is available. See the
|
||||
[FusionCache docs](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/CoreMethods.md)
|
||||
for more details.
|
||||
@@ -1,13 +1,7 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Utilities;
|
||||
|
||||
@@ -110,56 +104,7 @@ public static class StaticStore
|
||||
GlobalDomains.Add(GlobalEquivalentDomainsType.Atlassian, new List<string> { "atlassian.com", "bitbucket.org", "trello.com", "statuspage.io", "atlassian.net", "jira.com" });
|
||||
GlobalDomains.Add(GlobalEquivalentDomainsType.Pinterest, new List<string> { "pinterest.com", "pinterest.com.au", "pinterest.cl", "pinterest.de", "pinterest.dk", "pinterest.es", "pinterest.fr", "pinterest.co.uk", "pinterest.jp", "pinterest.co.kr", "pinterest.nz", "pinterest.pt", "pinterest.se" });
|
||||
#endregion
|
||||
|
||||
Plans = new List<Plan>
|
||||
{
|
||||
new EnterprisePlan(true),
|
||||
new EnterprisePlan(false),
|
||||
new TeamsStarterPlan(),
|
||||
new TeamsPlan(true),
|
||||
new TeamsPlan(false),
|
||||
|
||||
new Enterprise2023Plan(true),
|
||||
new Enterprise2023Plan(false),
|
||||
new Enterprise2020Plan(true),
|
||||
new Enterprise2020Plan(false),
|
||||
new TeamsStarterPlan2023(),
|
||||
new Teams2023Plan(true),
|
||||
new Teams2023Plan(false),
|
||||
new Teams2020Plan(true),
|
||||
new Teams2020Plan(false),
|
||||
new FamiliesPlan(),
|
||||
new FreePlan(),
|
||||
new CustomPlan(),
|
||||
|
||||
new Enterprise2019Plan(true),
|
||||
new Enterprise2019Plan(false),
|
||||
new Teams2019Plan(true),
|
||||
new Teams2019Plan(false),
|
||||
new Families2019Plan(),
|
||||
new Families2025Plan()
|
||||
}.ToImmutableList();
|
||||
}
|
||||
|
||||
public static IDictionary<GlobalEquivalentDomainsType, IEnumerable<string>> GlobalDomains { get; set; }
|
||||
[Obsolete("Use PricingClient.ListPlans to retrieve all plans.")]
|
||||
public static IEnumerable<Plan> Plans { get; }
|
||||
public static IEnumerable<SponsoredPlan> SponsoredPlans { get; set; } = new[]
|
||||
{
|
||||
new SponsoredPlan
|
||||
{
|
||||
PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise,
|
||||
SponsoredProductTierType = ProductTierType.Families,
|
||||
SponsoringProductTierType = ProductTierType.Enterprise,
|
||||
StripePlanId = "2021-family-for-enterprise-annually",
|
||||
UsersCanSponsor = (OrganizationUserOrganizationDetails org) =>
|
||||
org.PlanType.GetProductTier() == ProductTierType.Enterprise,
|
||||
}
|
||||
};
|
||||
|
||||
[Obsolete("Use PricingClient.GetPlan to retrieve a plan.")]
|
||||
public static Plan GetPlan(PlanType planType) => Plans.SingleOrDefault(p => p.Type == planType);
|
||||
|
||||
public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) =>
|
||||
SponsoredPlans.FirstOrDefault(p => p.PlanSponsorshipType == planSponsorshipType);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ public interface ICipherService
|
||||
Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key,
|
||||
long requestLength, Guid savingUserId, bool orgAdmin = false, DateTime? lastKnownRevisionDate = null);
|
||||
Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key, long requestLength,
|
||||
string attachmentId, Guid organizationShareId, DateTime? lastKnownRevisionDate = null);
|
||||
string attachmentId, Guid organizationShareId);
|
||||
Task DeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false);
|
||||
Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);
|
||||
Task<DeleteAttachmentResponseData> DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false);
|
||||
@@ -34,7 +34,7 @@ public interface ICipherService
|
||||
Task SoftDeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);
|
||||
Task RestoreAsync(CipherDetails cipherDetails, Guid restoringUserId, bool orgAdmin = false);
|
||||
Task<ICollection<CipherOrganizationDetails>> RestoreManyAsync(IEnumerable<Guid> cipherIds, Guid restoringUserId, Guid? organizationId = null, bool orgAdmin = false);
|
||||
Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId, DateTime? lastKnownRevisionDate = null);
|
||||
Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId);
|
||||
Task<AttachmentResponseData> GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId);
|
||||
Task<bool> ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData);
|
||||
Task ValidateBulkCollectionAssignmentAsync(IEnumerable<Guid> collectionIds, IEnumerable<Guid> cipherIds, Guid userId);
|
||||
|
||||
@@ -183,9 +183,8 @@ public class CipherService : ICipherService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachment, DateTime? lastKnownRevisionDate = null)
|
||||
public async Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachment)
|
||||
{
|
||||
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
|
||||
if (attachment == null)
|
||||
{
|
||||
throw new BadRequestException("Cipher attachment does not exist");
|
||||
@@ -290,11 +289,10 @@ public class CipherService : ICipherService
|
||||
}
|
||||
|
||||
public async Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key,
|
||||
long requestLength, string attachmentId, Guid organizationId, DateTime? lastKnownRevisionDate = null)
|
||||
long requestLength, string attachmentId, Guid organizationId)
|
||||
{
|
||||
try
|
||||
{
|
||||
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
|
||||
if (requestLength < 1)
|
||||
{
|
||||
throw new BadRequestException("No data to attach.");
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.AdminConsole.Utilities.DebuggingInstruments;
|
||||
using Bit.Core.Entities;
|
||||
@@ -671,7 +672,7 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<bool> ConfirmOrganizationUserAsync(OrganizationUser organizationUser)
|
||||
public async Task<bool> ConfirmOrganizationUserAsync(AcceptedOrganizationUserToConfirm organizationUserToConfirm)
|
||||
{
|
||||
await using var connection = new SqlConnection(_marsConnectionString);
|
||||
|
||||
@@ -679,10 +680,10 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
$"[{Schema}].[OrganizationUser_ConfirmById]",
|
||||
new
|
||||
{
|
||||
organizationUser.Id,
|
||||
organizationUser.UserId,
|
||||
Id = organizationUserToConfirm.OrganizationUserId,
|
||||
UserId = organizationUserToConfirm.UserId,
|
||||
RevisionDate = DateTime.UtcNow.Date,
|
||||
Key = organizationUser.Key
|
||||
Key = organizationUserToConfirm.Key
|
||||
});
|
||||
|
||||
return rowCount > 0;
|
||||
|
||||
@@ -148,4 +148,16 @@ public class OrganizationDomainRepository : Repository<OrganizationDomain, Guid>
|
||||
commandType: CommandType.StoredProcedure) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(string domainName, Guid? excludeOrganizationId = null)
|
||||
{
|
||||
await using var connection = new SqlConnection(ConnectionString);
|
||||
|
||||
var result = await connection.QueryFirstOrDefaultAsync<bool>(
|
||||
$"[{Schema}].[OrganizationDomain_HasVerifiedDomainWithBlockPolicy]",
|
||||
new { DomainName = domainName, ExcludeOrganizationId = excludeOrganizationId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using AutoMapper;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@@ -943,23 +944,24 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ConfirmOrganizationUserAsync(Core.Entities.OrganizationUser organizationUser)
|
||||
public async Task<bool> ConfirmOrganizationUserAsync(AcceptedOrganizationUserToConfirm organizationUserToConfirm)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
await using var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var result = await dbContext.OrganizationUsers
|
||||
.Where(ou => ou.Id == organizationUser.Id && ou.Status == OrganizationUserStatusType.Accepted)
|
||||
.Where(ou => ou.Id == organizationUserToConfirm.OrganizationUserId
|
||||
&& ou.Status == OrganizationUserStatusType.Accepted)
|
||||
.ExecuteUpdateAsync(x => x
|
||||
.SetProperty(y => y.Status, OrganizationUserStatusType.Confirmed)
|
||||
.SetProperty(y => y.Key, organizationUser.Key));
|
||||
.SetProperty(y => y.Key, organizationUserToConfirm.Key));
|
||||
|
||||
if (result <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdAsync(organizationUser.Id);
|
||||
await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdAsync(organizationUserToConfirm.OrganizationUserId);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ public class PolicyRepository : Repository<AdminConsoleEntities.Policy, Policy,
|
||||
UserId = u.Id
|
||||
}).ToListAsync();
|
||||
|
||||
// Combine results with provder lookup
|
||||
// Combine results with the provider lookup
|
||||
var allResults = acceptedUsers.Concat(invitedUsers)
|
||||
.Select(item => new OrganizationPolicyDetails
|
||||
{
|
||||
|
||||
@@ -177,5 +177,25 @@ public class OrganizationDomainRepository : Repository<Core.Entities.Organizatio
|
||||
return Mapper.Map<List<OrganizationDomain>>(verifiedDomains);
|
||||
}
|
||||
|
||||
public async Task<bool> HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(string domainName, Guid? excludeOrganizationId = null)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var query = from od in dbContext.OrganizationDomains
|
||||
join o in dbContext.Organizations on od.OrganizationId equals o.Id
|
||||
join p in dbContext.Policies on o.Id equals p.OrganizationId
|
||||
where od.DomainName == domainName
|
||||
&& od.VerifiedDate != null
|
||||
&& o.Enabled
|
||||
&& o.UsePolicies
|
||||
&& o.UseOrganizationDomains
|
||||
&& (!excludeOrganizationId.HasValue || o.Id != excludeOrganizationId.Value)
|
||||
&& p.Type == Core.AdminConsole.Enums.PolicyType.BlockClaimedDomainAccountCreation
|
||||
&& p.Enabled
|
||||
select od;
|
||||
|
||||
return await query.AnyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -645,7 +645,7 @@ public static class ServiceCollectionExtensions
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
||||
};
|
||||
|
||||
if (!globalSettings.UnifiedDeployment)
|
||||
if (!globalSettings.LiteDeployment)
|
||||
{
|
||||
// Trust the X-Forwarded-Host header of the nginx docker container
|
||||
try
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
CREATE PROCEDURE [dbo].[OrganizationDomain_HasVerifiedDomainWithBlockPolicy]
|
||||
@DomainName NVARCHAR(255),
|
||||
@ExcludeOrganizationId UNIQUEIDENTIFIER = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
-- Check if any organization has a verified domain matching the domain name
|
||||
-- with the BlockClaimedDomainAccountCreation policy enabled (Type = 19)
|
||||
-- If @ExcludeOrganizationId is provided, exclude that organization from the check
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM [dbo].[OrganizationDomain] OD
|
||||
INNER JOIN [dbo].[Organization] O
|
||||
ON OD.OrganizationId = O.Id
|
||||
INNER JOIN [dbo].[Policy] P
|
||||
ON O.Id = P.OrganizationId
|
||||
WHERE OD.DomainName = @DomainName
|
||||
AND OD.VerifiedDate IS NOT NULL
|
||||
AND O.Enabled = 1
|
||||
AND O.UsePolicies = 1
|
||||
AND O.UseOrganizationDomains = 1
|
||||
AND (@ExcludeOrganizationId IS NULL OR O.Id != @ExcludeOrganizationId)
|
||||
AND P.Type = 19 -- BlockClaimedDomainAccountCreation
|
||||
AND P.Enabled = 1
|
||||
)
|
||||
BEGIN
|
||||
SELECT CAST(1 AS BIT) AS HasBlockPolicy
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
SELECT CAST(0 AS BIT) AS HasBlockPolicy
|
||||
END
|
||||
END
|
||||
@@ -60,6 +60,7 @@ CREATE TABLE [dbo].[Organization] (
|
||||
[UseAdminSponsoredFamilies] BIT NOT NULL CONSTRAINT [DF_Organization_UseAdminSponsoredFamilies] DEFAULT (0),
|
||||
[SyncSeats] BIT NOT NULL CONSTRAINT [DF_Organization_SyncSeats] DEFAULT (0),
|
||||
[UseAutomaticUserConfirmation] BIT NOT NULL CONSTRAINT [DF_Organization_UseAutomaticUserConfirmation] DEFAULT (0),
|
||||
[MaxStorageGbIncreased] SMALLINT NULL,
|
||||
CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC)
|
||||
);
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
[SecurityState] VARCHAR (MAX) NULL,
|
||||
[SecurityVersion] INT NULL,
|
||||
[SignedPublicKey] VARCHAR (MAX) NULL,
|
||||
[MaxStorageGbIncreased] SMALLINT NULL,
|
||||
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)
|
||||
);
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ SELECT
|
||||
O.[UseSecretsManager],
|
||||
O.[Seats],
|
||||
O.[MaxCollections],
|
||||
O.[MaxStorageGb],
|
||||
COALESCE(O.[MaxStorageGbIncreased], O.[MaxStorageGb]) AS [MaxStorageGb],
|
||||
O.[Identifier],
|
||||
OU.[Key],
|
||||
OU.[ResetPasswordKey],
|
||||
|
||||
@@ -1,6 +1,66 @@
|
||||
CREATE VIEW [dbo].[OrganizationView]
|
||||
AS
|
||||
SELECT
|
||||
*
|
||||
[Id],
|
||||
[Identifier],
|
||||
[Name],
|
||||
[BusinessName],
|
||||
[BusinessAddress1],
|
||||
[BusinessAddress2],
|
||||
[BusinessAddress3],
|
||||
[BusinessCountry],
|
||||
[BusinessTaxNumber],
|
||||
[BillingEmail],
|
||||
[Plan],
|
||||
[PlanType],
|
||||
[Seats],
|
||||
[MaxCollections],
|
||||
[UsePolicies],
|
||||
[UseSso],
|
||||
[UseGroups],
|
||||
[UseDirectory],
|
||||
[UseEvents],
|
||||
[UseTotp],
|
||||
[Use2fa],
|
||||
[UseApi],
|
||||
[UseResetPassword],
|
||||
[SelfHost],
|
||||
[UsersGetPremium],
|
||||
[Storage],
|
||||
COALESCE([MaxStorageGbIncreased], [MaxStorageGb]) AS [MaxStorageGb],
|
||||
[Gateway],
|
||||
[GatewayCustomerId],
|
||||
[GatewaySubscriptionId],
|
||||
[ReferenceData],
|
||||
[Enabled],
|
||||
[LicenseKey],
|
||||
[PublicKey],
|
||||
[PrivateKey],
|
||||
[TwoFactorProviders],
|
||||
[ExpirationDate],
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[OwnersNotifiedOfAutoscaling],
|
||||
[MaxAutoscaleSeats],
|
||||
[UseKeyConnector],
|
||||
[UseScim],
|
||||
[UseCustomPermissions],
|
||||
[UseSecretsManager],
|
||||
[Status],
|
||||
[UsePasswordManager],
|
||||
[SmSeats],
|
||||
[SmServiceAccounts],
|
||||
[MaxAutoscaleSmSeats],
|
||||
[MaxAutoscaleSmServiceAccounts],
|
||||
[SecretsManagerBeta],
|
||||
[LimitCollectionCreation],
|
||||
[LimitCollectionDeletion],
|
||||
[LimitItemDeletion],
|
||||
[AllowAdminAccessToAllCollectionItems],
|
||||
[UseRiskInsights],
|
||||
[UseOrganizationDomains],
|
||||
[UseAdminSponsoredFamilies],
|
||||
[SyncSeats],
|
||||
[UseAutomaticUserConfirmation]
|
||||
FROM
|
||||
[dbo].[Organization]
|
||||
|
||||
@@ -23,7 +23,7 @@ SELECT
|
||||
O.[UseCustomPermissions],
|
||||
O.[Seats],
|
||||
O.[MaxCollections],
|
||||
O.[MaxStorageGb],
|
||||
COALESCE(O.[MaxStorageGbIncreased], O.[MaxStorageGb]) AS [MaxStorageGb],
|
||||
O.[Identifier],
|
||||
PO.[Key],
|
||||
O.[PublicKey],
|
||||
|
||||
@@ -1,6 +1,51 @@
|
||||
CREATE VIEW [dbo].[UserView]
|
||||
AS
|
||||
SELECT
|
||||
*
|
||||
[Id],
|
||||
[Name],
|
||||
[Email],
|
||||
[EmailVerified],
|
||||
[MasterPassword],
|
||||
[MasterPasswordHint],
|
||||
[Culture],
|
||||
[SecurityStamp],
|
||||
[TwoFactorProviders],
|
||||
[TwoFactorRecoveryCode],
|
||||
[EquivalentDomains],
|
||||
[ExcludedGlobalEquivalentDomains],
|
||||
[AccountRevisionDate],
|
||||
[Key],
|
||||
[PublicKey],
|
||||
[PrivateKey],
|
||||
[Premium],
|
||||
[PremiumExpirationDate],
|
||||
[RenewalReminderDate],
|
||||
[Storage],
|
||||
COALESCE([MaxStorageGbIncreased], [MaxStorageGb]) AS [MaxStorageGb],
|
||||
[Gateway],
|
||||
[GatewayCustomerId],
|
||||
[GatewaySubscriptionId],
|
||||
[ReferenceData],
|
||||
[LicenseKey],
|
||||
[ApiKey],
|
||||
[Kdf],
|
||||
[KdfIterations],
|
||||
[KdfMemory],
|
||||
[KdfParallelism],
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[ForcePasswordReset],
|
||||
[UsesKeyConnector],
|
||||
[FailedLoginCount],
|
||||
[LastFailedLoginDate],
|
||||
[AvatarColor],
|
||||
[LastPasswordChangeDate],
|
||||
[LastKdfChangeDate],
|
||||
[LastKeyRotationDate],
|
||||
[LastEmailChangeDate],
|
||||
[VerifyDevices],
|
||||
[SecurityState],
|
||||
[SecurityVersion],
|
||||
[SignedPublicKey]
|
||||
FROM
|
||||
[dbo].[User]
|
||||
|
||||
Reference in New Issue
Block a user