1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05:00

Merge branch 'ac/ac-2323/sql-automatic-data-migrations' into ac/ac-1682/ef-migrations

# Conflicts:
#	src/Core/Constants.cs
This commit is contained in:
Rui Tome
2024-04-10 15:41:52 +01:00
55 changed files with 939 additions and 226 deletions

View File

@ -40,7 +40,10 @@ jobs:
base_uri: https://ast.checkmarx.net/
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
additional_params: --report-format sarif --output-path . ${{ env.INCREMENTAL }}
additional_params: |
--report-format sarif \
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>2024.3.1</Version>
<Version>2024.4.0</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>

View File

@ -1,10 +1,15 @@
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Repositories;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
namespace Bit.Commercial.Core.AdminConsole.Providers;
@ -14,21 +19,28 @@ public class CreateProviderCommand : ICreateProviderCommand
private readonly IProviderUserRepository _providerUserRepository;
private readonly IProviderService _providerService;
private readonly IUserRepository _userRepository;
private readonly IProviderPlanRepository _providerPlanRepository;
private readonly IFeatureService _featureService;
public CreateProviderCommand(
IProviderRepository providerRepository,
IProviderUserRepository providerUserRepository,
IProviderService providerService,
IUserRepository userRepository)
IUserRepository userRepository,
IProviderPlanRepository providerPlanRepository,
IFeatureService featureService)
{
_providerRepository = providerRepository;
_providerUserRepository = providerUserRepository;
_providerService = providerService;
_userRepository = userRepository;
_providerPlanRepository = providerPlanRepository;
_featureService = featureService;
}
public async Task CreateMspAsync(Provider provider, string ownerEmail)
public async Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats)
{
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
if (owner == null)
{
@ -44,8 +56,24 @@ public class CreateProviderCommand : ICreateProviderCommand
Type = ProviderUserType.ProviderAdmin,
Status = ProviderUserStatusType.Confirmed,
};
if (isConsolidatedBillingEnabled)
{
var providerPlans = new List<ProviderPlan>
{
CreateProviderPlan(provider.Id, PlanType.TeamsMonthly, teamsMinimumSeats),
CreateProviderPlan(provider.Id, PlanType.EnterpriseMonthly, enterpriseMinimumSeats)
};
foreach (var providerPlan in providerPlans)
{
await _providerPlanRepository.CreateAsync(providerPlan);
}
}
await _providerUserRepository.CreateAsync(providerUser);
await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email);
}
public async Task CreateResellerAsync(Provider provider)
@ -60,4 +88,16 @@ public class CreateProviderCommand : ICreateProviderCommand
provider.UseEvents = true;
await _providerRepository.CreateAsync(provider);
}
private ProviderPlan CreateProviderPlan(Guid providerId, PlanType planType, int seatMinimum)
{
return new ProviderPlan
{
ProviderId = providerId,
PlanType = planType,
SeatMinimum = seatMinimum,
PurchasedSeats = 0,
AllocatedSeats = 0
};
}
}

View File

@ -382,10 +382,14 @@ public class ProviderService : IProviderService
organization.BillingEmail = provider.BillingEmail;
await _organizationRepository.ReplaceAsync(organization);
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
if (!string.IsNullOrEmpty(organization.GatewayCustomerId))
{
Email = provider.BillingEmail
});
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
{
Email = provider.BillingEmail
});
}
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added);
}

View File

@ -22,7 +22,7 @@ public class CreateProviderCommandTests
provider.Type = ProviderType.Msp;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CreateMspAsync(provider, default));
() => sutProvider.Sut.CreateMspAsync(provider, default, default, default));
Assert.Contains("Invalid owner.", exception.Message);
}
@ -34,7 +34,7 @@ public class CreateProviderCommandTests
var userRepository = sutProvider.GetDependency<IUserRepository>();
userRepository.GetByEmailAsync(user.Email).Returns(user);
await sutProvider.Sut.CreateMspAsync(provider, user.Email);
await sutProvider.Sut.CreateMspAsync(provider, user.Email, default, default);
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);

View File

@ -8,6 +8,8 @@ using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Repositories;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@ -34,6 +36,7 @@ public class ProvidersController : Controller
private readonly IUserService _userService;
private readonly ICreateProviderCommand _createProviderCommand;
private readonly IFeatureService _featureService;
private readonly IProviderPlanRepository _providerPlanRepository;
public ProvidersController(
IOrganizationRepository organizationRepository,
@ -47,7 +50,8 @@ public class ProvidersController : Controller
IReferenceEventService referenceEventService,
IUserService userService,
ICreateProviderCommand createProviderCommand,
IFeatureService featureService)
IFeatureService featureService,
IProviderPlanRepository providerPlanRepository)
{
_organizationRepository = organizationRepository;
_organizationService = organizationService;
@ -61,6 +65,7 @@ public class ProvidersController : Controller
_userService = userService;
_createProviderCommand = createProviderCommand;
_featureService = featureService;
_providerPlanRepository = providerPlanRepository;
}
[RequirePermission(Permission.Provider_List_View)]
@ -90,11 +95,13 @@ public class ProvidersController : Controller
});
}
public IActionResult Create(string ownerEmail = null)
public IActionResult Create(int teamsMinimumSeats, int enterpriseMinimumSeats, string ownerEmail = null)
{
return View(new CreateProviderModel
{
OwnerEmail = ownerEmail
OwnerEmail = ownerEmail,
TeamsMinimumSeats = teamsMinimumSeats,
EnterpriseMinimumSeats = enterpriseMinimumSeats
});
}
@ -112,7 +119,8 @@ public class ProvidersController : Controller
switch (provider.Type)
{
case ProviderType.Msp:
await _createProviderCommand.CreateMspAsync(provider, model.OwnerEmail);
await _createProviderCommand.CreateMspAsync(provider, model.OwnerEmail, model.TeamsMinimumSeats,
model.EnterpriseMinimumSeats);
break;
case ProviderType.Reseller:
await _createProviderCommand.CreateResellerAsync(provider);
@ -139,6 +147,7 @@ public class ProvidersController : Controller
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<IActionResult> Edit(Guid id)
{
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
var provider = await _providerRepository.GetByIdAsync(id);
if (provider == null)
{
@ -147,7 +156,12 @@ public class ProvidersController : Controller
var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
return View(new ProviderEditModel(provider, users, providerOrganizations));
if (isConsolidatedBillingEnabled)
{
var providerPlan = await _providerPlanRepository.GetByProviderId(id);
return View(new ProviderEditModel(provider, users, providerOrganizations, providerPlan));
}
return View(new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>()));
}
[HttpPost]
@ -156,6 +170,8 @@ public class ProvidersController : Controller
[RequirePermission(Permission.Provider_Edit)]
public async Task<IActionResult> Edit(Guid id, ProviderEditModel model)
{
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
var provider = await _providerRepository.GetByIdAsync(id);
if (provider == null)
{
@ -165,6 +181,15 @@ public class ProvidersController : Controller
model.ToProvider(provider);
await _providerRepository.ReplaceAsync(provider);
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
if (isConsolidatedBillingEnabled)
{
model.ToProviderPlan(providerPlans);
foreach (var providerPlan in providerPlans)
{
await _providerPlanRepository.ReplaceAsync(providerPlan);
}
}
return RedirectToAction("Edit", new { id });
}

View File

@ -24,6 +24,12 @@ public class CreateProviderModel : IValidatableObject
[Display(Name = "Primary Billing Email")]
public string BillingEmail { get; set; }
[Display(Name = "Teams minimum seats")]
public int TeamsMinimumSeats { get; set; }
[Display(Name = "Enterprise minimum seats")]
public int EnterpriseMinimumSeats { get; set; }
public virtual Provider ToProvider()
{
return new Provider()
@ -45,6 +51,16 @@ public class CreateProviderModel : IValidatableObject
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(OwnerEmail);
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
}
if (TeamsMinimumSeats < 0)
{
var teamsMinimumSeatsDisplayName = nameof(TeamsMinimumSeats).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(TeamsMinimumSeats);
yield return new ValidationResult($"The {teamsMinimumSeatsDisplayName} field can not be negative.");
}
if (EnterpriseMinimumSeats < 0)
{
var enterpriseMinimumSeatsDisplayName = nameof(EnterpriseMinimumSeats).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMinimumSeats);
yield return new ValidationResult($"The {enterpriseMinimumSeatsDisplayName} field can not be negative.");
}
break;
case ProviderType.Reseller:
if (string.IsNullOrWhiteSpace(Name))

View File

@ -1,6 +1,8 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Billing.Entities;
using Bit.Core.Enums;
namespace Bit.Admin.AdminConsole.Models;
@ -8,13 +10,16 @@ public class ProviderEditModel : ProviderViewModel
{
public ProviderEditModel() { }
public ProviderEditModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers, IEnumerable<ProviderOrganizationOrganizationDetails> organizations)
public ProviderEditModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers,
IEnumerable<ProviderOrganizationOrganizationDetails> organizations, IEnumerable<ProviderPlan> providerPlans)
: base(provider, providerUsers, organizations)
{
Name = provider.DisplayName();
BusinessName = provider.DisplayBusinessName();
BillingEmail = provider.BillingEmail;
BillingPhone = provider.BillingPhone;
TeamsMinimumSeats = GetMinimumSeats(providerPlans, PlanType.TeamsMonthly);
EnterpriseMinimumSeats = GetMinimumSeats(providerPlans, PlanType.EnterpriseMonthly);
}
[Display(Name = "Billing Email")]
@ -24,12 +29,38 @@ public class ProviderEditModel : ProviderViewModel
[Display(Name = "Business Name")]
public string BusinessName { get; set; }
public string Name { get; set; }
[Display(Name = "Teams minimum seats")]
public int TeamsMinimumSeats { get; set; }
[Display(Name = "Enterprise minimum seats")]
public int EnterpriseMinimumSeats { get; set; }
[Display(Name = "Events")]
public IEnumerable<ProviderPlan> ToProviderPlan(IEnumerable<ProviderPlan> existingProviderPlans)
{
var providerPlans = existingProviderPlans.ToList();
foreach (var existingProviderPlan in providerPlans)
{
existingProviderPlan.SeatMinimum = existingProviderPlan.PlanType switch
{
PlanType.TeamsMonthly => TeamsMinimumSeats,
PlanType.EnterpriseMonthly => EnterpriseMinimumSeats,
_ => existingProviderPlan.SeatMinimum
};
}
return providerPlans;
}
public Provider ToProvider(Provider existingProvider)
{
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant()?.Trim();
return existingProvider;
}
private int GetMinimumSeats(IEnumerable<ProviderPlan> providerPlans, PlanType planType)
{
return (from providerPlan in providerPlans where providerPlan.PlanType == planType select (int)providerPlan.SeatMinimum).FirstOrDefault();
}
}

View File

@ -1,6 +1,8 @@
@using Bit.SharedWeb.Utilities
@using Bit.Core.AdminConsole.Enums.Provider
@using Bit.Core
@model CreateProviderModel
@inject Bit.Core.Services.IFeatureService FeatureService
@{
ViewData["Title"] = "Create Provider";
}
@ -39,6 +41,23 @@
<label asp-for="OwnerEmail"></label>
<input type="text" class="form-control" asp-for="OwnerEmail">
</div>
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
{
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="TeamsMinimumSeats"></label>
<input type="number" class="form-control" asp-for="TeamsMinimumSeats">
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="EnterpriseMinimumSeats"></label>
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
</div>
</div>
</div>
}
</div>
<div id="@($"info-{(int)ProviderType.Reseller}")" class="form-group @(Model.Type != ProviderType.Reseller ? "d-none" : string.Empty)">

View File

@ -1,5 +1,7 @@
@using Bit.Admin.Enums;
@using Bit.Core
@inject Bit.Admin.Services.IAccessControlService AccessControlService
@inject Bit.Core.Services.IFeatureService FeatureService
@model ProviderEditModel
@{
@ -41,6 +43,23 @@
</div>
</div>
</div>
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
{
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="TeamsMinimumSeats"></label>
<input type="number" class="form-control" asp-for="TeamsMinimumSeats">
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="EnterpriseMinimumSeats"></label>
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
</div>
</div>
</div>
}
</form>
@await Html.PartialAsync("Organizations", Model)
@if (canEdit)

View File

@ -80,7 +80,6 @@ public class OrganizationDomainController : Controller
var organizationDomain = new OrganizationDomain
{
OrganizationId = orgId,
Txt = model.Txt,
DomainName = model.DomainName.ToLower()
};

View File

@ -4,9 +4,6 @@ namespace Bit.Api.AdminConsole.Models.Request;
public class OrganizationDomainRequestModel
{
[Required]
public string Txt { get; set; }
[Required]
public string DomainName { get; set; }
}

View File

@ -42,11 +42,11 @@ public class PushController : Controller
Prefix(model.UserId), Prefix(model.Identifier), model.Type);
}
[HttpDelete("{id}")]
public async Task Delete(string id)
[HttpPost("delete")]
public async Task PostDelete([FromBody] PushDeviceRequestModel model)
{
CheckUsage();
await _pushRegistrationService.DeleteRegistrationAsync(Prefix(id));
await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id), model.Type);
}
[HttpPut("add-organization")]
@ -54,7 +54,8 @@ public class PushController : Controller
{
CheckUsage();
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(
model.DeviceIds.Select(d => Prefix(d)), Prefix(model.OrganizationId));
model.Devices.Select(d => new KeyValuePair<string, Core.Enums.DeviceType>(Prefix(d.Id), d.Type)),
Prefix(model.OrganizationId));
}
[HttpPut("delete-organization")]
@ -62,7 +63,8 @@ public class PushController : Controller
{
CheckUsage();
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(
model.DeviceIds.Select(d => Prefix(d)), Prefix(model.OrganizationId));
model.Devices.Select(d => new KeyValuePair<string, Core.Enums.DeviceType>(Prefix(d.Id), d.Type)),
Prefix(model.OrganizationId));
}
[HttpPost("send")]

View File

@ -85,7 +85,7 @@ public class PeopleAccessPoliciesRequestModel
if (!policies.All(ap => ap.Read && ap.Write))
{
throw new BadRequestException("Service account access must be Can read, write");
throw new BadRequestException("Machine account access must be Can read, write");
}
return new ServiceAccountPeopleAccessPolicies

View File

@ -560,7 +560,7 @@ public class CiphersController : Controller
[HttpPut("{id}/collections")]
[HttpPost("{id}/collections")]
public async Task PutCollections(Guid id, [FromBody] CipherCollectionsRequestModel model)
public async Task<CipherResponseModel> PutCollections(Guid id, [FromBody] CipherCollectionsRequestModel model)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await GetByIdAsync(id, userId);
@ -572,6 +572,10 @@ public class CiphersController : Controller
await _cipherService.SaveCollectionsAsync(cipher,
model.CollectionIds.Select(c => new Guid(c)), userId, false);
var updatedCipherCollections = await GetByIdAsync(id, userId);
var response = new CipherResponseModel(updatedCipherCollections, _globalSettings);
return response;
}
[HttpPut("{id}/collections-admin")]
@ -1106,6 +1110,33 @@ public class CiphersController : Controller
});
}
/// <summary>
/// Returns true if the user is an admin or owner of an organization with unassigned ciphers (i.e. ciphers that
/// are not assigned to a collection).
/// </summary>
/// <returns></returns>
[HttpGet("has-unassigned-ciphers")]
public async Task<bool> HasUnassignedCiphers()
{
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
var adminOrganizations = _currentContext.Organizations
.Where(o => o.Type is OrganizationUserType.Admin or OrganizationUserType.Owner &&
orgAbilities.ContainsKey(o.Id) && orgAbilities[o.Id].FlexibleCollections);
foreach (var org in adminOrganizations)
{
var unassignedCiphers = await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(org.Id);
// We only care about non-deleted ciphers
if (unassignedCiphers.Any(c => c.DeletedDate == null))
{
return true;
}
}
return false;
}
private void ValidateAttachment()
{
if (!Request?.ContentType.Contains("multipart/") ?? true)

View File

@ -75,7 +75,7 @@ public class PayPalController : Controller
if (string.IsNullOrEmpty(transactionModel.TransactionId))
{
_logger.LogError("PayPal IPN: Transaction ID is missing");
_logger.LogWarning("PayPal IPN: Transaction ID is missing");
return Ok();
}

View File

@ -453,7 +453,7 @@ public class StripeController : Controller
}
else if (parsedEvent.Type.Equals(HandledStripeWebhook.ChargeRefunded))
{
var charge = await _stripeEventService.GetCharge(parsedEvent);
var charge = await _stripeEventService.GetCharge(parsedEvent, true, ["refunds"]);
var chargeTransaction = await _transactionRepository.GetByGatewayIdAsync(
GatewayType.Stripe, charge.Id);
if (chargeTransaction == null)

View File

@ -5,6 +5,7 @@ using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.Extensions.Logging;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
@ -50,26 +51,16 @@ public class CreateOrganizationDomainCommand : ICreateOrganizationDomainCommand
throw new ConflictException("A domain already exists for this organization.");
}
try
{
if (await _dnsResolverService.ResolveAsync(organizationDomain.DomainName, organizationDomain.Txt))
{
organizationDomain.SetVerifiedDate();
}
}
catch (Exception e)
{
_logger.LogError(e, "Error verifying Organization domain.");
}
// Generate and set DNS TXT Record
// DNS-Based Service Discovery RFC: https://www.ietf.org/rfc/rfc6763.txt; see section 6.1
// Google uses 43 chars for their TXT record value: https://support.google.com/a/answer/2716802
// A random 44 character string was used here to keep parity with prior client-side generation of 47 characters
organizationDomain.Txt = string.Join("=", "bw", CoreHelpers.RandomString(44));
organizationDomain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
organizationDomain.SetLastCheckedDate();
var orgDomain = await _organizationDomainRepository.CreateAsync(organizationDomain);
await _eventService.LogOrganizationDomainEventAsync(orgDomain, EventType.OrganizationDomain_Added);
await _eventService.LogOrganizationDomainEventAsync(orgDomain,
orgDomain.VerifiedDate != null ? EventType.OrganizationDomain_Verified : EventType.OrganizationDomain_NotVerified);
return orgDomain;
}

View File

@ -4,6 +4,6 @@ namespace Bit.Core.AdminConsole.Providers.Interfaces;
public interface ICreateProviderCommand
{
Task CreateMspAsync(Provider provider, string ownerEmail);
Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats);
Task CreateResellerAsync(Provider provider);
}

View File

@ -681,8 +681,8 @@ public class OrganizationService : IOrganizationService
await _organizationUserRepository.CreateAsync(orgUser);
var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(deviceIds,
var devices = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(devices,
organization.Id.ToString());
await _pushNotificationService.PushSyncOrgKeysAsync(ownerId);
}
@ -1932,17 +1932,19 @@ public class OrganizationService : IOrganizationService
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
{
var deviceIds = await GetUserDeviceIdsAsync(userId);
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(deviceIds,
var devices = await GetUserDeviceIdsAsync(userId);
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices,
organizationId.ToString());
await _pushNotificationService.PushSyncOrgKeysAsync(userId);
}
private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
private async Task<IEnumerable<KeyValuePair<string, DeviceType>>> GetUserDeviceIdsAsync(Guid userId)
{
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
return devices.Where(d => !string.IsNullOrWhiteSpace(d.PushToken)).Select(d => d.Id.ToString());
return devices
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
.Select(d => new KeyValuePair<string, DeviceType>(d.Id.ToString(), d.Type));
}
public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null)
@ -2037,7 +2039,7 @@ public class OrganizationService : IOrganizationService
if (!plan.SecretsManager.HasAdditionalServiceAccountOption && upgrade.AdditionalServiceAccounts > 0)
{
throw new BadRequestException("Plan does not allow additional Service Accounts.");
throw new BadRequestException("Plan does not allow additional Machine Accounts.");
}
if ((plan.Product == ProductType.TeamsStarter &&
@ -2050,7 +2052,7 @@ public class OrganizationService : IOrganizationService
if (upgrade.AdditionalServiceAccounts.GetValueOrDefault() < 0)
{
throw new BadRequestException("You can't subtract Service Accounts!");
throw new BadRequestException("You can't subtract Machine Accounts!");
}
switch (plan.SecretsManager.HasAdditionalSeatsOption)

View File

@ -131,6 +131,9 @@ public static class FeatureFlagKeys
public const string PM5864DollarThreshold = "PM-5864-dollar-threshold";
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
public const string ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners";
public const string EnableConsolidatedBilling = "enable-consolidated-billing";
public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section";
public const string UnassignedItemsBanner = "unassigned-items-banner";
public static List<string> GetAllKeys()
{

View File

@ -0,0 +1,11 @@
namespace Bit.Core.Enums;
public enum NotificationHubType
{
General = 0,
Android = 1,
iOS = 2,
GeneralWeb = 3,
GeneralBrowserExtension = 4,
GeneralDesktop = 5
}

View File

@ -28,14 +28,24 @@ public enum PlanType : byte
EnterpriseMonthly2020 = 10,
[Display(Name = "Enterprise (Annually) 2020")]
EnterpriseAnnually2020 = 11,
[Display(Name = "Teams (Monthly) 2023")]
TeamsMonthly2023 = 12,
[Display(Name = "Teams (Annually) 2023")]
TeamsAnnually2023 = 13,
[Display(Name = "Enterprise (Monthly) 2023")]
EnterpriseMonthly2023 = 14,
[Display(Name = "Enterprise (Annually) 2023")]
EnterpriseAnnually2023 = 15,
[Display(Name = "Teams Starter 2023")]
TeamsStarter2023 = 16,
[Display(Name = "Teams (Monthly)")]
TeamsMonthly = 12,
TeamsMonthly = 17,
[Display(Name = "Teams (Annually)")]
TeamsAnnually = 13,
TeamsAnnually = 18,
[Display(Name = "Enterprise (Monthly)")]
EnterpriseMonthly = 14,
EnterpriseMonthly = 19,
[Display(Name = "Enterprise (Annually)")]
EnterpriseAnnually = 15,
EnterpriseAnnually = 20,
[Display(Name = "Teams Starter")]
TeamsStarter = 16,
TeamsStarter = 21,
}

View File

@ -6,7 +6,7 @@
<td class="content-block"
style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;"
valign="top">
Your organization has reached the Secrets Manager service accounts limit of {{MaxServiceAccountsCount}}. New service accounts cannot be created
Your organization has reached the Secrets Manager machine accounts limit of {{MaxServiceAccountsCount}}. New machine accounts cannot be created
</td>
</tr>
<tr

View File

@ -1,5 +1,5 @@
{{#>BasicTextLayout}}
Your organization has reached the Secrets Manager service accounts limit of {{MaxServiceAccountsCount}}. New service accounts cannot be created
Your organization has reached the Secrets Manager machine accounts limit of {{MaxServiceAccountsCount}}. New machine accounts cannot be created
For more information, please refer to the following help article: https://bitwarden.com/help/managing-users
{{/BasicTextLayout}}

View File

@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
namespace Bit.Core.Models.Api;
public class PushDeviceRequestModel
{
[Required]
public string Id { get; set; }
[Required]
public DeviceType Type { get; set; }
}

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
namespace Bit.Core.Models.Api;
@ -7,14 +8,14 @@ public class PushUpdateRequestModel
public PushUpdateRequestModel()
{ }
public PushUpdateRequestModel(IEnumerable<string> deviceIds, string organizationId)
public PushUpdateRequestModel(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
{
DeviceIds = deviceIds;
Devices = devices.Select(d => new PushDeviceRequestModel { Id = d.Key, Type = d.Value });
OrganizationId = organizationId;
}
[Required]
public IEnumerable<string> DeviceIds { get; set; }
public IEnumerable<PushDeviceRequestModel> Devices { get; set; }
[Required]
public string OrganizationId { get; set; }
}

View File

@ -2,7 +2,7 @@
namespace Bit.Core.Models.StaticStore.Plans;
public record EnterprisePlan : Models.StaticStore.Plan
public record EnterprisePlan : Plan
{
public EnterprisePlan(bool isAnnual)
{
@ -44,7 +44,7 @@ public record EnterprisePlan : Models.StaticStore.Plan
{
BaseSeats = 0;
BasePrice = 0;
BaseServiceAccount = 200;
BaseServiceAccount = 50;
HasAdditionalSeatsOption = true;
HasAdditionalServiceAccountOption = true;
@ -55,16 +55,16 @@ public record EnterprisePlan : Models.StaticStore.Plan
if (isAnnual)
{
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-annually";
SeatPrice = 144;
AdditionalPricePerServiceAccount = 6;
AdditionalPricePerServiceAccount = 12;
}
else
{
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-monthly";
SeatPrice = 13;
AdditionalPricePerServiceAccount = 0.5M;
AdditionalPricePerServiceAccount = 1;
}
}
}

View File

@ -0,0 +1,102 @@
using Bit.Core.Enums;
namespace Bit.Core.Models.StaticStore.Plans;
public record Enterprise2023Plan : Plan
{
public Enterprise2023Plan(bool isAnnual)
{
Type = isAnnual ? PlanType.EnterpriseAnnually2023 : PlanType.EnterpriseMonthly2023;
Product = ProductType.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;
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;
}
}
}
}

View File

@ -2,7 +2,7 @@
namespace Bit.Core.Models.StaticStore.Plans;
public record TeamsPlan : Models.StaticStore.Plan
public record TeamsPlan : Plan
{
public TeamsPlan(bool isAnnual)
{
@ -37,7 +37,7 @@ public record TeamsPlan : Models.StaticStore.Plan
{
BaseSeats = 0;
BasePrice = 0;
BaseServiceAccount = 50;
BaseServiceAccount = 20;
HasAdditionalSeatsOption = true;
HasAdditionalServiceAccountOption = true;
@ -48,16 +48,16 @@ public record TeamsPlan : Models.StaticStore.Plan
if (isAnnual)
{
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-annually";
SeatPrice = 72;
AdditionalPricePerServiceAccount = 6;
AdditionalPricePerServiceAccount = 12;
}
else
{
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-monthly";
SeatPrice = 7;
AdditionalPricePerServiceAccount = 0.5M;
AdditionalPricePerServiceAccount = 1;
}
}
}

View File

@ -0,0 +1,96 @@
using Bit.Core.Enums;
namespace Bit.Core.Models.StaticStore.Plans;
public record Teams2023Plan : Plan
{
public Teams2023Plan(bool isAnnual)
{
Type = isAnnual ? PlanType.TeamsAnnually2023 : PlanType.TeamsMonthly2023;
Product = ProductType.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;
}
}
}
}

View File

@ -36,7 +36,7 @@ public record TeamsStarterPlan : Plan
{
BaseSeats = 0;
BasePrice = 0;
BaseServiceAccount = 50;
BaseServiceAccount = 20;
HasAdditionalSeatsOption = true;
HasAdditionalServiceAccountOption = true;
@ -45,9 +45,9 @@ public record TeamsStarterPlan : Plan
AllowServiceAccountsAutoscale = true;
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-monthly";
SeatPrice = 7;
AdditionalPricePerServiceAccount = 0.5M;
AdditionalPricePerServiceAccount = 1;
}
}

View File

@ -0,0 +1,72 @@
using Bit.Core.Enums;
namespace Bit.Core.Models.StaticStore.Plans;
public record TeamsStarterPlan2023 : Plan
{
public TeamsStarterPlan2023()
{
Type = PlanType.TeamsStarter2023;
Product = ProductType.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;
}
}
}

View File

@ -118,7 +118,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
}
catch (Exception e)
{
_logger.LogError(e, $"Error encountered notifying organization owners of service accounts limit reached.");
_logger.LogError(e, $"Error encountered notifying organization owners of machine accounts limit reached.");
}
}
@ -253,12 +253,12 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
// Check if the organization has unlimited service accounts
if (organization.SmServiceAccounts == null)
{
throw new BadRequestException("Organization has no service accounts limit, no need to adjust service accounts");
throw new BadRequestException("Organization has no machine accounts limit, no need to adjust machine accounts");
}
if (update.Autoscaling && update.SmServiceAccounts.Value < organization.SmServiceAccounts.Value)
{
throw new BadRequestException("Cannot use autoscaling to subtract service accounts.");
throw new BadRequestException("Cannot use autoscaling to subtract machine accounts.");
}
// Check plan maximum service accounts
@ -267,7 +267,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
{
var planMaxServiceAccounts = plan.SecretsManager.BaseServiceAccount +
plan.SecretsManager.MaxAdditionalServiceAccount.GetValueOrDefault();
throw new BadRequestException($"You have reached the maximum number of service accounts ({planMaxServiceAccounts}) for this plan.");
throw new BadRequestException($"You have reached the maximum number of machine accounts ({planMaxServiceAccounts}) for this plan.");
}
// Check autoscale maximum service accounts
@ -275,21 +275,21 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
update.SmServiceAccounts.Value > update.MaxAutoscaleSmServiceAccounts.Value)
{
var message = update.Autoscaling
? "Secrets Manager service account limit has been reached."
: "Cannot set max service accounts autoscaling below service account amount.";
? "Secrets Manager machine account limit has been reached."
: "Cannot set max machine accounts autoscaling below machine account amount.";
throw new BadRequestException(message);
}
// Check minimum service accounts included with plan
if (plan.SecretsManager.BaseServiceAccount > update.SmServiceAccounts.Value)
{
throw new BadRequestException($"Plan has a minimum of {plan.SecretsManager.BaseServiceAccount} service accounts.");
throw new BadRequestException($"Plan has a minimum of {plan.SecretsManager.BaseServiceAccount} machine accounts.");
}
// Check minimum service accounts required by business logic
if (update.SmServiceAccounts.Value <= 0)
{
throw new BadRequestException("You must have at least 1 service account.");
throw new BadRequestException("You must have at least 1 machine account.");
}
// Check minimum service accounts currently in use by the organization
@ -298,8 +298,8 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
var currentServiceAccounts = await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id);
if (currentServiceAccounts > update.SmServiceAccounts)
{
throw new BadRequestException($"Your organization currently has {currentServiceAccounts} service accounts. " +
$"You cannot decrease your subscription below your current service account usage.");
throw new BadRequestException($"Your organization currently has {currentServiceAccounts} machine accounts. " +
$"You cannot decrease your subscription below your current machine account usage.");
}
}
}
@ -346,18 +346,18 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
if (update.SmServiceAccounts.HasValue && update.MaxAutoscaleSmServiceAccounts.Value < update.SmServiceAccounts.Value)
{
throw new BadRequestException(
$"Cannot set max service accounts autoscaling below current service accounts count.");
$"Cannot set max machine accounts autoscaling below current machine accounts count.");
}
if (!plan.SecretsManager.AllowServiceAccountsAutoscale)
{
throw new BadRequestException("Your plan does not allow service accounts autoscaling.");
throw new BadRequestException("Your plan does not allow machine accounts autoscaling.");
}
if (plan.SecretsManager.MaxServiceAccounts.HasValue && update.MaxAutoscaleSmServiceAccounts.Value > plan.SecretsManager.MaxServiceAccounts)
{
throw new BadRequestException(string.Concat(
$"Your plan has a service account limit of {plan.SecretsManager.MaxServiceAccounts}, ",
$"Your plan has a machine account limit of {plan.SecretsManager.MaxServiceAccounts}, ",
$"but you have specified a max autoscale count of {update.MaxAutoscaleSmServiceAccounts}.",
"Reduce your max autoscale count."));
}

View File

@ -330,9 +330,9 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
if (currentServiceAccounts > newPlanServiceAccounts)
{
throw new BadRequestException(
$"Your organization currently has {currentServiceAccounts} service accounts. " +
$"Your new plan only allows {newSecretsManagerPlan.SecretsManager.MaxServiceAccounts} service accounts. " +
"Remove some service accounts or increase your subscription.");
$"Your organization currently has {currentServiceAccounts} machine accounts. " +
$"Your new plan only allows {newSecretsManagerPlan.SecretsManager.MaxServiceAccounts} machine accounts. " +
"Remove some machine accounts or increase your subscription.");
}
}
}

View File

@ -6,7 +6,7 @@ public interface IPushRegistrationService
{
Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
string identifier, DeviceType type);
Task DeleteRegistrationAsync(string deviceId);
Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId);
Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId);
Task DeleteRegistrationAsync(string deviceId, DeviceType type);
Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId);
Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId);
}

View File

@ -38,13 +38,13 @@ public class DeviceService : IDeviceService
public async Task ClearTokenAsync(Device device)
{
await _deviceRepository.ClearPushTokenAsync(device.Id);
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString(), device.Type);
}
public async Task DeleteAsync(Device device)
{
await _deviceRepository.DeleteAsync(device);
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString(), device.Type);
}
public async Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,

View File

@ -951,7 +951,7 @@ public class HandlebarsMailService : IMailService
public async Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount,
IEnumerable<string> ownerEmails)
{
var message = CreateDefaultMessage($"{organization.DisplayName()} Secrets Manager Service Accounts Limit Reached", ownerEmails);
var message = CreateDefaultMessage($"{organization.DisplayName()} Secrets Manager Machine Accounts Limit Reached", ownerEmails);
var model = new OrganizationServiceAccountsMaxReachedViewModel
{
OrganizationId = organization.Id,

View File

@ -43,7 +43,8 @@ public class MultiServicePushNotificationService : IPushNotificationService
}
else
{
if (CoreHelpers.SettingHasValue(globalSettings.NotificationHub.ConnectionString))
var generalHub = globalSettings.NotificationHubs?.FirstOrDefault(h => h.HubType == NotificationHubType.General);
if (CoreHelpers.SettingHasValue(generalHub?.ConnectionString))
{
_services.Add(new NotificationHubPushNotificationService(installationDeviceRepository,
globalSettings, httpContextAccessor, hubLogger));

View File

@ -20,8 +20,9 @@ public class NotificationHubPushNotificationService : IPushNotificationService
private readonly IInstallationDeviceRepository _installationDeviceRepository;
private readonly GlobalSettings _globalSettings;
private readonly IHttpContextAccessor _httpContextAccessor;
private NotificationHubClient _client = null;
private ILogger _logger;
private readonly List<NotificationHubClient> _clients = [];
private readonly bool _enableTracing = false;
private readonly ILogger _logger;
public NotificationHubPushNotificationService(
IInstallationDeviceRepository installationDeviceRepository,
@ -32,10 +33,18 @@ public class NotificationHubPushNotificationService : IPushNotificationService
_installationDeviceRepository = installationDeviceRepository;
_globalSettings = globalSettings;
_httpContextAccessor = httpContextAccessor;
_client = NotificationHubClient.CreateClientFromConnectionString(
_globalSettings.NotificationHub.ConnectionString,
_globalSettings.NotificationHub.HubName,
_globalSettings.NotificationHub.EnableSendTracing);
foreach (var hub in globalSettings.NotificationHubs)
{
var client = NotificationHubClient.CreateClientFromConnectionString(
hub.ConnectionString,
hub.HubName,
hub.EnableSendTracing);
_clients.Add(client);
_enableTracing = _enableTracing || hub.EnableSendTracing;
}
_logger = logger;
}
@ -255,16 +264,31 @@ public class NotificationHubPushNotificationService : IPushNotificationService
private async Task SendPayloadAsync(string tag, PushType type, object payload)
{
var outcome = await _client.SendTemplateNotificationAsync(
new Dictionary<string, string>
{
{ "type", ((byte)type).ToString() },
{ "payload", JsonSerializer.Serialize(payload) }
}, tag);
if (_globalSettings.NotificationHub.EnableSendTracing)
var tasks = new List<Task<NotificationOutcome>>();
foreach (var client in _clients)
{
_logger.LogInformation("Azure Notification Hub Tracking ID: {id} | {type} push notification with {success} successes and {failure} failures with a payload of {@payload} and result of {@results}",
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
var task = client.SendTemplateNotificationAsync(
new Dictionary<string, string>
{
{ "type", ((byte)type).ToString() },
{ "payload", JsonSerializer.Serialize(payload) }
}, tag);
tasks.Add(task);
}
await Task.WhenAll(tasks);
if (_enableTracing)
{
for (var i = 0; i < tasks.Count; i++)
{
if (_clients[i].EnableTestSend)
{
var outcome = await tasks[i];
_logger.LogInformation("Azure Notification Hub Tracking ID: {id} | {type} push notification with {success} successes and {failure} failures with a payload of {@payload} and result of {@results}",
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
}
}
}
}

View File

@ -3,6 +3,7 @@ using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Microsoft.Azure.NotificationHubs;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Services;
@ -10,18 +11,36 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
{
private readonly IInstallationDeviceRepository _installationDeviceRepository;
private readonly GlobalSettings _globalSettings;
private NotificationHubClient _client = null;
private readonly ILogger<NotificationHubPushRegistrationService> _logger;
private Dictionary<NotificationHubType, NotificationHubClient> _clients = [];
public NotificationHubPushRegistrationService(
IInstallationDeviceRepository installationDeviceRepository,
GlobalSettings globalSettings)
GlobalSettings globalSettings,
ILogger<NotificationHubPushRegistrationService> logger)
{
_installationDeviceRepository = installationDeviceRepository;
_globalSettings = globalSettings;
_client = NotificationHubClient.CreateClientFromConnectionString(
_globalSettings.NotificationHub.ConnectionString,
_globalSettings.NotificationHub.HubName);
_logger = logger;
// Is this dirty to do in the ctor?
void addHub(NotificationHubType type)
{
var hubRegistration = globalSettings.NotificationHubs.FirstOrDefault(
h => h.HubType == type && h.EnableRegistration);
if (hubRegistration != null)
{
var client = NotificationHubClient.CreateClientFromConnectionString(
hubRegistration.ConnectionString,
hubRegistration.HubName,
hubRegistration.EnableSendTracing);
_clients.Add(type, client);
}
}
addHub(NotificationHubType.General);
addHub(NotificationHubType.iOS);
addHub(NotificationHubType.Android);
}
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
@ -84,7 +103,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate,
userId, identifier);
await _client.CreateOrUpdateInstallationAsync(installation);
await GetClient(type).CreateOrUpdateInstallationAsync(installation);
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
@ -119,11 +138,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
installation.Templates.Add(fullTemplateId, template);
}
public async Task DeleteRegistrationAsync(string deviceId)
public async Task DeleteRegistrationAsync(string deviceId, DeviceType deviceType)
{
try
{
await _client.DeleteInstallationAsync(deviceId);
await GetClient(deviceType).DeleteInstallationAsync(deviceId);
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{
await _installationDeviceRepository.DeleteAsync(new InstallationDeviceEntity(deviceId));
@ -135,31 +154,31 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
}
}
public async Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
public async Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
{
await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Add, $"organizationId:{organizationId}");
if (deviceIds.Any() && InstallationDeviceEntity.IsInstallationDeviceId(deviceIds.First()))
await PatchTagsForUserDevicesAsync(devices, UpdateOperationType.Add, $"organizationId:{organizationId}");
if (devices.Any() && InstallationDeviceEntity.IsInstallationDeviceId(devices.First().Key))
{
var entities = deviceIds.Select(e => new InstallationDeviceEntity(e));
var entities = devices.Select(e => new InstallationDeviceEntity(e.Key));
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
}
}
public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
{
await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Remove,
await PatchTagsForUserDevicesAsync(devices, UpdateOperationType.Remove,
$"organizationId:{organizationId}");
if (deviceIds.Any() && InstallationDeviceEntity.IsInstallationDeviceId(deviceIds.First()))
if (devices.Any() && InstallationDeviceEntity.IsInstallationDeviceId(devices.First().Key))
{
var entities = deviceIds.Select(e => new InstallationDeviceEntity(e));
var entities = devices.Select(e => new InstallationDeviceEntity(e.Key));
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
}
}
private async Task PatchTagsForUserDevicesAsync(IEnumerable<string> deviceIds, UpdateOperationType op,
private async Task PatchTagsForUserDevicesAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, UpdateOperationType op,
string tag)
{
if (!deviceIds.Any())
if (!devices.Any())
{
return;
}
@ -179,11 +198,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
operation.Path += $"/{tag}";
}
foreach (var id in deviceIds)
foreach (var device in devices)
{
try
{
await _client.PatchInstallationAsync(id, new List<PartialUpdateOperation> { operation });
await GetClient(device.Value).PatchInstallationAsync(device.Key, new List<PartialUpdateOperation> { operation });
}
catch (Exception e) when (e.InnerException == null || !e.InnerException.Message.Contains("(404) Not Found"))
{
@ -191,4 +210,54 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
}
}
}
private NotificationHubClient GetClient(DeviceType deviceType)
{
var hubType = NotificationHubType.General;
switch (deviceType)
{
case DeviceType.Android:
hubType = NotificationHubType.Android;
break;
case DeviceType.iOS:
hubType = NotificationHubType.iOS;
break;
case DeviceType.ChromeExtension:
case DeviceType.FirefoxExtension:
case DeviceType.OperaExtension:
case DeviceType.EdgeExtension:
case DeviceType.VivaldiExtension:
case DeviceType.SafariExtension:
hubType = NotificationHubType.GeneralBrowserExtension;
break;
case DeviceType.WindowsDesktop:
case DeviceType.MacOsDesktop:
case DeviceType.LinuxDesktop:
hubType = NotificationHubType.GeneralDesktop;
break;
case DeviceType.ChromeBrowser:
case DeviceType.FirefoxBrowser:
case DeviceType.OperaBrowser:
case DeviceType.EdgeBrowser:
case DeviceType.IEBrowser:
case DeviceType.UnknownBrowser:
case DeviceType.SafariBrowser:
case DeviceType.VivaldiBrowser:
hubType = NotificationHubType.GeneralWeb;
break;
default:
break;
}
if (!_clients.ContainsKey(hubType))
{
_logger.LogWarning("No hub client for '{0}'. Using general hub instead.", hubType);
hubType = NotificationHubType.General;
if (!_clients.ContainsKey(hubType))
{
throw new Exception("No general hub client found.");
}
}
return _clients[hubType];
}
}

View File

@ -38,30 +38,37 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi
await SendAsync(HttpMethod.Post, "push/register", requestModel);
}
public async Task DeleteRegistrationAsync(string deviceId)
public async Task DeleteRegistrationAsync(string deviceId, DeviceType type)
{
await SendAsync(HttpMethod.Delete, string.Concat("push/", deviceId));
var requestModel = new PushDeviceRequestModel
{
Id = deviceId,
Type = type,
};
await SendAsync(HttpMethod.Post, "push/delete", requestModel);
}
public async Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
public async Task AddUserRegistrationOrganizationAsync(
IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
{
if (!deviceIds.Any())
if (!devices.Any())
{
return;
}
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
var requestModel = new PushUpdateRequestModel(devices, organizationId);
await SendAsync(HttpMethod.Put, "push/add-organization", requestModel);
}
public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
public async Task DeleteUserRegistrationOrganizationAsync(
IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
{
if (!deviceIds.Any())
if (!devices.Any())
{
return;
}
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
var requestModel = new PushUpdateRequestModel(devices, organizationId);
await SendAsync(HttpMethod.Put, "push/delete-organization", requestModel);
}
}

View File

@ -4,7 +4,7 @@ namespace Bit.Core.Services;
public class NoopPushRegistrationService : IPushRegistrationService
{
public Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
public Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
{
return Task.FromResult(0);
}
@ -15,12 +15,12 @@ public class NoopPushRegistrationService : IPushRegistrationService
return Task.FromResult(0);
}
public Task DeleteRegistrationAsync(string deviceId)
public Task DeleteRegistrationAsync(string deviceId, DeviceType deviceType)
{
return Task.FromResult(0);
}
public Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
public Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
{
return Task.FromResult(0);
}

View File

@ -1,4 +1,5 @@
using Bit.Core.Auth.Settings;
using Bit.Core.Enums;
using Bit.Core.Settings.LoggingSettings;
namespace Bit.Core.Settings;
@ -64,7 +65,7 @@ public class GlobalSettings : IGlobalSettings
public virtual SentrySettings Sentry { get; set; } = new SentrySettings();
public virtual SyslogSettings Syslog { get; set; } = new SyslogSettings();
public virtual ILogLevelSettings MinLogLevel { get; set; } = new LogLevelSettings();
public virtual NotificationHubSettings NotificationHub { get; set; } = new NotificationHubSettings();
public virtual List<NotificationHubSettings> NotificationHubs { get; set; } = new();
public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings();
public virtual DuoSettings Duo { get; set; } = new DuoSettings();
public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings();
@ -414,12 +415,16 @@ public class GlobalSettings : IGlobalSettings
set => _connectionString = value.Trim('"');
}
public string HubName { get; set; }
/// <summary>
/// Enables TestSend on the Azure Notification Hub, which allows tracing of the request through the hub and to the platform-specific push notification service (PNS).
/// Enabling this will result in delayed responses because the Hub must wait on delivery to the PNS. This should ONLY be enabled in a non-production environment, as results are throttled.
/// </summary>
public bool EnableSendTracing { get; set; } = false;
/// <summary>
/// At least one hub configuration should have registration enabled, preferably the General hub as a safety net.
/// </summary>
public bool EnableRegistration { get; set; }
public NotificationHubType HubType { get; set; }
}
public class YubicoSettings

View File

@ -114,8 +114,13 @@ public static class StaticStore
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(),

View File

@ -26,7 +26,20 @@ public class OrganizationMapperProfile : Profile
{
public OrganizationMapperProfile()
{
CreateMap<Core.AdminConsole.Entities.Organization, Organization>().ReverseMap();
CreateMap<Core.AdminConsole.Entities.Organization, Organization>()
.ForMember(org => org.Ciphers, opt => opt.Ignore())
.ForMember(org => org.OrganizationUsers, opt => opt.Ignore())
.ForMember(org => org.Groups, opt => opt.Ignore())
.ForMember(org => org.Policies, opt => opt.Ignore())
.ForMember(org => org.Collections, opt => opt.Ignore())
.ForMember(org => org.SsoConfigs, opt => opt.Ignore())
.ForMember(org => org.SsoUsers, opt => opt.Ignore())
.ForMember(org => org.Transactions, opt => opt.Ignore())
.ForMember(org => org.ApiKeys, opt => opt.Ignore())
.ForMember(org => org.Connections, opt => opt.Ignore())
.ForMember(org => org.Domains, opt => opt.Ignore())
.ReverseMap();
CreateProjection<Organization, SelfHostedOrganizationDetails>()
.ForMember(sd => sd.CollectionCount, opt => opt.MapFrom(o => o.Collections.Count))
.ForMember(sd => sd.GroupCount, opt => opt.MapFrom(o => o.Groups.Count))

View File

@ -50,9 +50,10 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
{
var dbContext = GetDatabaseContext(scope);
var organizations = await GetDbSet(dbContext)
.Select(e => e.OrganizationUsers
.Where(ou => ou.UserId == userId)
.Select(ou => ou.Organization))
.SelectMany(e => e.OrganizationUsers
.Where(ou => ou.UserId == userId))
.Include(ou => ou.Organization)
.Select(ou => ou.Organization)
.ToListAsync();
return Mapper.Map<List<Core.AdminConsole.Entities.Organization>>(organizations);
}

View File

@ -7,7 +7,6 @@ using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using NSubstitute.ReturnsExtensions;
using Xunit;
@ -25,9 +24,6 @@ public class CreateOrganizationDomainCommandTests
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetDomainByOrgIdAndDomainNameAsync(orgDomain.OrganizationId, orgDomain.DomainName)
.ReturnsNull();
sutProvider.GetDependency<IDnsResolverService>()
.ResolveAsync(orgDomain.DomainName, orgDomain.Txt)
.Returns(false);
orgDomain.SetNextRunDate(12);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.CreateAsync(orgDomain)
@ -38,12 +34,12 @@ public class CreateOrganizationDomainCommandTests
Assert.Equal(orgDomain.Id, result.Id);
Assert.Equal(orgDomain.OrganizationId, result.OrganizationId);
Assert.NotNull(result.LastCheckedDate);
Assert.Null(result.LastCheckedDate);
Assert.Equal(orgDomain.Txt, result.Txt);
Assert.Equal(orgDomain.Txt.Length == 47, result.Txt.Length == 47);
Assert.Equal(orgDomain.NextRunDate, result.NextRunDate);
await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationDomainEventAsync(Arg.Any<OrganizationDomain>(), EventType.OrganizationDomain_Added);
await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationDomainEventAsync(Arg.Any<OrganizationDomain>(), Arg.Is<EventType>(x => x == EventType.OrganizationDomain_NotVerified));
}
[Theory, BitAutoData]
@ -79,52 +75,4 @@ public class CreateOrganizationDomainCommandTests
var exception = await Assert.ThrowsAsync<ConflictException>(requestAction);
Assert.Contains("A domain already exists for this organization.", exception.Message);
}
[Theory, BitAutoData]
public async Task CreateAsync_ShouldNotSetVerifiedDate_WhenDomainCannotBeResolved(OrganizationDomain orgDomain,
SutProvider<CreateOrganizationDomainCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetClaimedDomainsByDomainNameAsync(orgDomain.DomainName)
.Returns(new List<OrganizationDomain>());
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetDomainByOrgIdAndDomainNameAsync(orgDomain.OrganizationId, orgDomain.DomainName)
.ReturnsNull();
sutProvider.GetDependency<IDnsResolverService>()
.ResolveAsync(orgDomain.DomainName, orgDomain.Txt)
.Throws(new DnsQueryException(""));
sutProvider.GetDependency<IOrganizationDomainRepository>()
.CreateAsync(orgDomain)
.Returns(orgDomain);
await sutProvider.Sut.CreateAsync(orgDomain);
Assert.Null(orgDomain.VerifiedDate);
}
[Theory, BitAutoData]
public async Task CreateAsync_ShouldSetVerifiedDateAndLogEvent_WhenDomainIsResolved(OrganizationDomain orgDomain,
SutProvider<CreateOrganizationDomainCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetClaimedDomainsByDomainNameAsync(orgDomain.DomainName)
.Returns(new List<OrganizationDomain>());
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetDomainByOrgIdAndDomainNameAsync(orgDomain.OrganizationId, orgDomain.DomainName)
.ReturnsNull();
sutProvider.GetDependency<IDnsResolverService>()
.ResolveAsync(orgDomain.DomainName, orgDomain.Txt)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.CreateAsync(orgDomain)
.Returns(orgDomain);
var result = await sutProvider.Sut.CreateAsync(orgDomain);
Assert.NotNull(result.VerifiedDate);
await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationDomainEventAsync(Arg.Any<OrganizationDomain>(), EventType.OrganizationDomain_Added);
await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationDomainEventAsync(Arg.Any<OrganizationDomain>(), Arg.Is<EventType>(x => x == EventType.OrganizationDomain_Verified));
}
}

View File

@ -410,7 +410,7 @@ public class OrganizationServiceTests
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpAsync(signup));
Assert.Contains("Plan does not allow additional Service Accounts.", exception.Message);
Assert.Contains("Plan does not allow additional Machine Accounts.", exception.Message);
}
[Theory]
@ -444,7 +444,7 @@ public class OrganizationServiceTests
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpAsync(signup));
Assert.Contains("You can't subtract Service Accounts!", exception.Message);
Assert.Contains("You can't subtract Machine Accounts!", exception.Message);
}
[Theory]
@ -2208,7 +2208,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
AdditionalSeats = 3
};
var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup));
Assert.Contains("Plan does not allow additional Service Accounts.", exception.Message);
Assert.Contains("Plan does not allow additional Machine Accounts.", exception.Message);
}
[Theory]
@ -2249,7 +2249,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
AdditionalSeats = 5
};
var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup));
Assert.Contains("You can't subtract Service Accounts!", exception.Message);
Assert.Contains("You can't subtract Machine Accounts!", exception.Message);
}
[Theory]

View File

@ -447,7 +447,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustServiceAccounts(1);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("Organization has no service accounts limit, no need to adjust service accounts", exception.Message);
Assert.Contains("Organization has no machine accounts limit, no need to adjust machine accounts", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider);
}
@ -460,7 +460,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustServiceAccounts(-2);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("Cannot use autoscaling to subtract service accounts.", exception.Message);
Assert.Contains("Cannot use autoscaling to subtract machine accounts.", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider);
}
@ -475,7 +475,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustServiceAccounts(1);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("You have reached the maximum number of service accounts (3) for this plan",
Assert.Contains("You have reached the maximum number of machine accounts (3) for this plan",
exception.Message, StringComparison.InvariantCultureIgnoreCase);
await VerifyDependencyNotCalledAsync(sutProvider);
}
@ -492,7 +492,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustServiceAccounts(2);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("Secrets Manager service account limit has been reached.", exception.Message);
Assert.Contains("Secrets Manager machine account limit has been reached.", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider);
}
@ -516,7 +516,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("Cannot set max service accounts autoscaling below service account amount", exception.Message);
Assert.Contains("Cannot set max machine accounts autoscaling below machine account amount", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider);
}
@ -526,7 +526,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
const int newSmServiceAccounts = 199;
const int newSmServiceAccounts = 49;
organization.SmServiceAccounts = newSmServiceAccounts - 10;
@ -537,7 +537,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("Plan has a minimum of 200 service accounts", exception.Message);
Assert.Contains("Plan has a minimum of 50 machine accounts", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider);
}
@ -570,7 +570,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
.Returns(currentServiceAccounts);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("Your organization currently has 301 service accounts. You cannot decrease your subscription below your current service account usage", exception.Message);
Assert.Contains("Your organization currently has 301 machine accounts. You cannot decrease your subscription below your current machine account usage", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider);
}
@ -648,7 +648,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var update = new SecretsManagerSubscriptionUpdate(organization, false) { MaxAutoscaleSmServiceAccounts = 3 };
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("Your plan does not allow service accounts autoscaling.", exception.Message);
Assert.Contains("Your plan does not allow machine accounts autoscaling.", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider);
}

View File

@ -192,7 +192,7 @@ public class UpgradeOrganizationPlanCommandTests
.GetServiceAccountCountByOrganizationIdAsync(organization.Id).Returns(currentServiceAccounts);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
Assert.Contains($"Your organization currently has {currentServiceAccounts} service accounts. Your new plan only allows", exception.Message);
Assert.Contains($"Your organization currently has {currentServiceAccounts} machine accounts. Your new plan only allows", exception.Message);
sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs().ReplaceAndUpdateCacheAsync(default);
}

View File

@ -1,6 +1,7 @@
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
@ -11,16 +12,19 @@ public class NotificationHubPushRegistrationServiceTests
private readonly NotificationHubPushRegistrationService _sut;
private readonly IInstallationDeviceRepository _installationDeviceRepository;
private readonly ILogger<NotificationHubPushRegistrationService> _logger;
private readonly GlobalSettings _globalSettings;
public NotificationHubPushRegistrationServiceTests()
{
_installationDeviceRepository = Substitute.For<IInstallationDeviceRepository>();
_logger = Substitute.For<ILogger<NotificationHubPushRegistrationService>>();
_globalSettings = new GlobalSettings();
_sut = new NotificationHubPushRegistrationService(
_installationDeviceRepository,
_globalSettings
_globalSettings,
_logger
);
}

View File

@ -13,7 +13,7 @@ public class StaticStoreTests
var plans = StaticStore.Plans.ToList();
Assert.NotNull(plans);
Assert.NotEmpty(plans);
Assert.Equal(17, plans.Count);
Assert.Equal(22, plans.Count);
}
[Theory]

View File

@ -0,0 +1,141 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.IntegrationTestCommon.Factories;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Microsoft.AspNetCore.TestHost;
using Xunit;
namespace Bit.Identity.IntegrationTest.Endpoints;
public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFactory>
{
private readonly IdentityApplicationFactory _factory;
private readonly IUserRepository _userRepository;
private readonly IUserService _userService;
public IdentityServerTwoFactorTests(IdentityApplicationFactory factory)
{
_factory = factory;
_userRepository = _factory.GetService<IUserRepository>();
_userService = _factory.GetService<IUserService>();
}
[Theory, BitAutoData]
public async Task TokenEndpoint_UserTwoFactorRequired_NoTwoFactorProvided_Fails(string deviceId)
{
// Arrange
var username = "test+2farequired@email.com";
var twoFactor = """{"1": { "Enabled": true, "MetaData": { "Email": "test+2farequired@email.com"}}}""";
await CreateUserAsync(_factory.Server, username, deviceId, async () =>
{
var user = await _userRepository.GetByEmailAsync(username);
user.TwoFactorProviders = twoFactor;
await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
});
// Act
var context = await PostLoginAsync(_factory.Server, username, deviceId);
// Assert
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var root = body.RootElement;
var error = AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String).GetString();
Assert.Equal("Two factor required.", error);
}
[Theory, BitAutoData]
public async Task TokenEndpoint_OrgTwoFactorRequired_NoTwoFactorProvided_Fails(string deviceId)
{
// Arrange
var username = "test+org2farequired@email.com";
// use valid length keys so DuoWeb.SignRequest doesn't throw
// ikey: 20, skey: 40, akey: 40
var orgTwoFactor =
"""{"6":{"Enabled":true,"MetaData":{"IKey":"DIEFB13LB49IEB3459N2","SKey":"0ZnsZHav0KcNPBZTS6EOUwqLPoB0sfMd5aJeWExQ","Host":"api-example.duosecurity.com"}}}""";
var server = _factory.WithWebHostBuilder(builder =>
{
builder.UseSetting("globalSettings:Duo:AKey", "WJHB374KM3N5hglO9hniwbkibg$789EfbhNyLpNq1");
}).Server;
await CreateUserAsync(server, username, deviceId, async () =>
{
var user = await _userRepository.GetByEmailAsync(username);
var organizationRepository = _factory.Services.GetService<IOrganizationRepository>();
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
Use2fa = true,
TwoFactorProviders = orgTwoFactor,
});
await _factory.Services.GetService<IOrganizationUserRepository>()
.CreateAsync(new OrganizationUser
{
UserId = user.Id,
OrganizationId = organization.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
});
});
// Act
var context = await PostLoginAsync(server, username, deviceId);
// Assert
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var root = body.RootElement;
var error = AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String).GetString();
Assert.Equal("Two factor required.", error);
}
private async Task CreateUserAsync(TestServer server, string username, string deviceId,
Func<Task> twoFactorSetup)
{
// Register user
await _factory.RegisterAsync(new RegisterRequestModel
{
Email = username,
MasterPasswordHash = "master_password_hash"
});
// Add two factor
if (twoFactorSetup != null)
{
await twoFactorSetup();
}
}
private async Task<HttpContext> PostLoginAsync(TestServer server, string username, string deviceId,
Action<HttpContext> extraConfiguration = null)
{
return await server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "scope", "api offline_access" },
{ "client_id", "web" },
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
{ "deviceIdentifier", deviceId },
{ "deviceName", "firefox" },
{ "grant_type", "password" },
{ "username", username },
{ "password", "master_password_hash" },
}), context => context.SetAuthEmail(username));
}
private static string DeviceTypeAsString(DeviceType deviceType)
{
return ((int)deviceType).ToString();
}
}

View File

@ -1,9 +1,11 @@
using Bit.Core.Entities;
using AutoMapper;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Test.AutoFixture.Attributes;
using Bit.Infrastructure.EFIntegration.Test.AutoFixture;
using Bit.Infrastructure.EFIntegration.Test.Repositories.EqualityComparers;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
using Xunit;
using EfRepo = Bit.Infrastructure.EntityFramework.Repositories;
using Organization = Bit.Core.AdminConsole.Entities.Organization;
@ -13,6 +15,13 @@ namespace Bit.Infrastructure.EFIntegration.Test.Repositories;
public class OrganizationRepositoryTests
{
[Fact]
public void ValidateOrganizationMappings_ReturnsSuccess()
{
var config = new MapperConfiguration(cfg => cfg.AddProfile<OrganizationMapperProfile>());
config.AssertConfigurationIsValid();
}
[CiSkippedTheory, EfOrganizationAutoData]
public async Task CreateAsync_Works_DataMatches(
Organization organization,