1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-01 16:12:49 -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/ base_uri: https://ast.checkmarx.net/
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }} cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }} 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 - name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6

View File

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Version>2024.3.1</Version> <Version>2024.4.0</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace> <RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings> <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.Enums.Provider;
using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; 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.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services;
namespace Bit.Commercial.Core.AdminConsole.Providers; namespace Bit.Commercial.Core.AdminConsole.Providers;
@ -14,21 +19,28 @@ public class CreateProviderCommand : ICreateProviderCommand
private readonly IProviderUserRepository _providerUserRepository; private readonly IProviderUserRepository _providerUserRepository;
private readonly IProviderService _providerService; private readonly IProviderService _providerService;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IProviderPlanRepository _providerPlanRepository;
private readonly IFeatureService _featureService;
public CreateProviderCommand( public CreateProviderCommand(
IProviderRepository providerRepository, IProviderRepository providerRepository,
IProviderUserRepository providerUserRepository, IProviderUserRepository providerUserRepository,
IProviderService providerService, IProviderService providerService,
IUserRepository userRepository) IUserRepository userRepository,
IProviderPlanRepository providerPlanRepository,
IFeatureService featureService)
{ {
_providerRepository = providerRepository; _providerRepository = providerRepository;
_providerUserRepository = providerUserRepository; _providerUserRepository = providerUserRepository;
_providerService = providerService; _providerService = providerService;
_userRepository = userRepository; _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); var owner = await _userRepository.GetByEmailAsync(ownerEmail);
if (owner == null) if (owner == null)
{ {
@ -44,8 +56,24 @@ public class CreateProviderCommand : ICreateProviderCommand
Type = ProviderUserType.ProviderAdmin, Type = ProviderUserType.ProviderAdmin,
Status = ProviderUserStatusType.Confirmed, 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 _providerUserRepository.CreateAsync(providerUser);
await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email); await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email);
} }
public async Task CreateResellerAsync(Provider provider) public async Task CreateResellerAsync(Provider provider)
@ -60,4 +88,16 @@ public class CreateProviderCommand : ICreateProviderCommand
provider.UseEvents = true; provider.UseEvents = true;
await _providerRepository.CreateAsync(provider); 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; organization.BillingEmail = provider.BillingEmail;
await _organizationRepository.ReplaceAsync(organization); 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); await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added);
} }

View File

@ -22,7 +22,7 @@ public class CreateProviderCommandTests
provider.Type = ProviderType.Msp; provider.Type = ProviderType.Msp;
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CreateMspAsync(provider, default)); () => sutProvider.Sut.CreateMspAsync(provider, default, default, default));
Assert.Contains("Invalid owner.", exception.Message); Assert.Contains("Invalid owner.", exception.Message);
} }
@ -34,7 +34,7 @@ public class CreateProviderCommandTests
var userRepository = sutProvider.GetDependency<IUserRepository>(); var userRepository = sutProvider.GetDependency<IUserRepository>();
userRepository.GetByEmailAsync(user.Email).Returns(user); 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<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email); 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.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Repositories;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
@ -34,6 +36,7 @@ public class ProvidersController : Controller
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly ICreateProviderCommand _createProviderCommand; private readonly ICreateProviderCommand _createProviderCommand;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IProviderPlanRepository _providerPlanRepository;
public ProvidersController( public ProvidersController(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
@ -47,7 +50,8 @@ public class ProvidersController : Controller
IReferenceEventService referenceEventService, IReferenceEventService referenceEventService,
IUserService userService, IUserService userService,
ICreateProviderCommand createProviderCommand, ICreateProviderCommand createProviderCommand,
IFeatureService featureService) IFeatureService featureService,
IProviderPlanRepository providerPlanRepository)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationService = organizationService; _organizationService = organizationService;
@ -61,6 +65,7 @@ public class ProvidersController : Controller
_userService = userService; _userService = userService;
_createProviderCommand = createProviderCommand; _createProviderCommand = createProviderCommand;
_featureService = featureService; _featureService = featureService;
_providerPlanRepository = providerPlanRepository;
} }
[RequirePermission(Permission.Provider_List_View)] [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 return View(new CreateProviderModel
{ {
OwnerEmail = ownerEmail OwnerEmail = ownerEmail,
TeamsMinimumSeats = teamsMinimumSeats,
EnterpriseMinimumSeats = enterpriseMinimumSeats
}); });
} }
@ -112,7 +119,8 @@ public class ProvidersController : Controller
switch (provider.Type) switch (provider.Type)
{ {
case ProviderType.Msp: case ProviderType.Msp:
await _createProviderCommand.CreateMspAsync(provider, model.OwnerEmail); await _createProviderCommand.CreateMspAsync(provider, model.OwnerEmail, model.TeamsMinimumSeats,
model.EnterpriseMinimumSeats);
break; break;
case ProviderType.Reseller: case ProviderType.Reseller:
await _createProviderCommand.CreateResellerAsync(provider); await _createProviderCommand.CreateResellerAsync(provider);
@ -139,6 +147,7 @@ public class ProvidersController : Controller
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
public async Task<IActionResult> Edit(Guid id) public async Task<IActionResult> Edit(Guid id)
{ {
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
var provider = await _providerRepository.GetByIdAsync(id); var provider = await _providerRepository.GetByIdAsync(id);
if (provider == null) if (provider == null)
{ {
@ -147,7 +156,12 @@ public class ProvidersController : Controller
var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id); var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);
var providerOrganizations = await _providerOrganizationRepository.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] [HttpPost]
@ -156,6 +170,8 @@ public class ProvidersController : Controller
[RequirePermission(Permission.Provider_Edit)] [RequirePermission(Permission.Provider_Edit)]
public async Task<IActionResult> Edit(Guid id, ProviderEditModel model) 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); var provider = await _providerRepository.GetByIdAsync(id);
if (provider == null) if (provider == null)
{ {
@ -165,6 +181,15 @@ public class ProvidersController : Controller
model.ToProvider(provider); model.ToProvider(provider);
await _providerRepository.ReplaceAsync(provider); await _providerRepository.ReplaceAsync(provider);
await _applicationCacheService.UpsertProviderAbilityAsync(provider); await _applicationCacheService.UpsertProviderAbilityAsync(provider);
if (isConsolidatedBillingEnabled)
{
model.ToProviderPlan(providerPlans);
foreach (var providerPlan in providerPlans)
{
await _providerPlanRepository.ReplaceAsync(providerPlan);
}
}
return RedirectToAction("Edit", new { id }); return RedirectToAction("Edit", new { id });
} }

View File

@ -24,6 +24,12 @@ public class CreateProviderModel : IValidatableObject
[Display(Name = "Primary Billing Email")] [Display(Name = "Primary Billing Email")]
public string BillingEmail { get; set; } 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() public virtual Provider ToProvider()
{ {
return new Provider() return new Provider()
@ -45,6 +51,16 @@ public class CreateProviderModel : IValidatableObject
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(OwnerEmail); var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(OwnerEmail);
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required."); 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; break;
case ProviderType.Reseller: case ProviderType.Reseller:
if (string.IsNullOrWhiteSpace(Name)) if (string.IsNullOrWhiteSpace(Name))

View File

@ -1,6 +1,8 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Billing.Entities;
using Bit.Core.Enums;
namespace Bit.Admin.AdminConsole.Models; namespace Bit.Admin.AdminConsole.Models;
@ -8,13 +10,16 @@ public class ProviderEditModel : ProviderViewModel
{ {
public ProviderEditModel() { } 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) : base(provider, providerUsers, organizations)
{ {
Name = provider.DisplayName(); Name = provider.DisplayName();
BusinessName = provider.DisplayBusinessName(); BusinessName = provider.DisplayBusinessName();
BillingEmail = provider.BillingEmail; BillingEmail = provider.BillingEmail;
BillingPhone = provider.BillingPhone; BillingPhone = provider.BillingPhone;
TeamsMinimumSeats = GetMinimumSeats(providerPlans, PlanType.TeamsMonthly);
EnterpriseMinimumSeats = GetMinimumSeats(providerPlans, PlanType.EnterpriseMonthly);
} }
[Display(Name = "Billing Email")] [Display(Name = "Billing Email")]
@ -24,12 +29,38 @@ public class ProviderEditModel : ProviderViewModel
[Display(Name = "Business Name")] [Display(Name = "Business Name")]
public string BusinessName { get; set; } public string BusinessName { get; set; }
public string Name { 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")] [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) public Provider ToProvider(Provider existingProvider)
{ {
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim(); existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant()?.Trim(); existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant()?.Trim();
return existingProvider; 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.SharedWeb.Utilities
@using Bit.Core.AdminConsole.Enums.Provider @using Bit.Core.AdminConsole.Enums.Provider
@using Bit.Core
@model CreateProviderModel @model CreateProviderModel
@inject Bit.Core.Services.IFeatureService FeatureService
@{ @{
ViewData["Title"] = "Create Provider"; ViewData["Title"] = "Create Provider";
} }
@ -39,6 +41,23 @@
<label asp-for="OwnerEmail"></label> <label asp-for="OwnerEmail"></label>
<input type="text" class="form-control" asp-for="OwnerEmail"> <input type="text" class="form-control" asp-for="OwnerEmail">
</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>
}
</div> </div>
<div id="@($"info-{(int)ProviderType.Reseller}")" class="form-group @(Model.Type != ProviderType.Reseller ? "d-none" : string.Empty)"> <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.Admin.Enums;
@using Bit.Core
@inject Bit.Admin.Services.IAccessControlService AccessControlService @inject Bit.Admin.Services.IAccessControlService AccessControlService
@inject Bit.Core.Services.IFeatureService FeatureService
@model ProviderEditModel @model ProviderEditModel
@{ @{
@ -41,6 +43,23 @@
</div> </div>
</div> </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> </form>
@await Html.PartialAsync("Organizations", Model) @await Html.PartialAsync("Organizations", Model)
@if (canEdit) @if (canEdit)

View File

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

View File

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

View File

@ -42,11 +42,11 @@ public class PushController : Controller
Prefix(model.UserId), Prefix(model.Identifier), model.Type); Prefix(model.UserId), Prefix(model.Identifier), model.Type);
} }
[HttpDelete("{id}")] [HttpPost("delete")]
public async Task Delete(string id) public async Task PostDelete([FromBody] PushDeviceRequestModel model)
{ {
CheckUsage(); CheckUsage();
await _pushRegistrationService.DeleteRegistrationAsync(Prefix(id)); await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id), model.Type);
} }
[HttpPut("add-organization")] [HttpPut("add-organization")]
@ -54,7 +54,8 @@ public class PushController : Controller
{ {
CheckUsage(); CheckUsage();
await _pushRegistrationService.AddUserRegistrationOrganizationAsync( 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")] [HttpPut("delete-organization")]
@ -62,7 +63,8 @@ public class PushController : Controller
{ {
CheckUsage(); CheckUsage();
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync( 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")] [HttpPost("send")]

View File

@ -85,7 +85,7 @@ public class PeopleAccessPoliciesRequestModel
if (!policies.All(ap => ap.Read && ap.Write)) 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 return new ServiceAccountPeopleAccessPolicies

View File

@ -560,7 +560,7 @@ public class CiphersController : Controller
[HttpPut("{id}/collections")] [HttpPut("{id}/collections")]
[HttpPost("{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 userId = _userService.GetProperUserId(User).Value;
var cipher = await GetByIdAsync(id, userId); var cipher = await GetByIdAsync(id, userId);
@ -572,6 +572,10 @@ public class CiphersController : Controller
await _cipherService.SaveCollectionsAsync(cipher, await _cipherService.SaveCollectionsAsync(cipher,
model.CollectionIds.Select(c => new Guid(c)), userId, false); 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")] [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() private void ValidateAttachment()
{ {
if (!Request?.ContentType.Contains("multipart/") ?? true) if (!Request?.ContentType.Contains("multipart/") ?? true)

View File

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

View File

@ -453,7 +453,7 @@ public class StripeController : Controller
} }
else if (parsedEvent.Type.Equals(HandledStripeWebhook.ChargeRefunded)) 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( var chargeTransaction = await _transactionRepository.GetByGatewayIdAsync(
GatewayType.Stripe, charge.Id); GatewayType.Stripe, charge.Id);
if (chargeTransaction == null) if (chargeTransaction == null)

View File

@ -5,6 +5,7 @@ using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
@ -50,26 +51,16 @@ public class CreateOrganizationDomainCommand : ICreateOrganizationDomainCommand
throw new ConflictException("A domain already exists for this organization."); throw new ConflictException("A domain already exists for this organization.");
} }
try // Generate and set DNS TXT Record
{ // DNS-Based Service Discovery RFC: https://www.ietf.org/rfc/rfc6763.txt; see section 6.1
if (await _dnsResolverService.ResolveAsync(organizationDomain.DomainName, organizationDomain.Txt)) // 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.SetVerifiedDate(); organizationDomain.Txt = string.Join("=", "bw", CoreHelpers.RandomString(44));
}
}
catch (Exception e)
{
_logger.LogError(e, "Error verifying Organization domain.");
}
organizationDomain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval); organizationDomain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
organizationDomain.SetLastCheckedDate();
var orgDomain = await _organizationDomainRepository.CreateAsync(organizationDomain); var orgDomain = await _organizationDomainRepository.CreateAsync(organizationDomain);
await _eventService.LogOrganizationDomainEventAsync(orgDomain, EventType.OrganizationDomain_Added); await _eventService.LogOrganizationDomainEventAsync(orgDomain, EventType.OrganizationDomain_Added);
await _eventService.LogOrganizationDomainEventAsync(orgDomain,
orgDomain.VerifiedDate != null ? EventType.OrganizationDomain_Verified : EventType.OrganizationDomain_NotVerified);
return orgDomain; return orgDomain;
} }

View File

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

View File

@ -681,8 +681,8 @@ public class OrganizationService : IOrganizationService
await _organizationUserRepository.CreateAsync(orgUser); await _organizationUserRepository.CreateAsync(orgUser);
var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value); var devices = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(deviceIds, await _pushRegistrationService.AddUserRegistrationOrganizationAsync(devices,
organization.Id.ToString()); organization.Id.ToString());
await _pushNotificationService.PushSyncOrgKeysAsync(ownerId); await _pushNotificationService.PushSyncOrgKeysAsync(ownerId);
} }
@ -1932,17 +1932,19 @@ public class OrganizationService : IOrganizationService
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId) private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
{ {
var deviceIds = await GetUserDeviceIdsAsync(userId); var devices = await GetUserDeviceIdsAsync(userId);
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(deviceIds, await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices,
organizationId.ToString()); organizationId.ToString());
await _pushNotificationService.PushSyncOrgKeysAsync(userId); 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); 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) public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null)
@ -2037,7 +2039,7 @@ public class OrganizationService : IOrganizationService
if (!plan.SecretsManager.HasAdditionalServiceAccountOption && upgrade.AdditionalServiceAccounts > 0) 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 && if ((plan.Product == ProductType.TeamsStarter &&
@ -2050,7 +2052,7 @@ public class OrganizationService : IOrganizationService
if (upgrade.AdditionalServiceAccounts.GetValueOrDefault() < 0) 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) 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 PM5864DollarThreshold = "PM-5864-dollar-threshold";
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email"; public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
public const string ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners"; 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() 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, EnterpriseMonthly2020 = 10,
[Display(Name = "Enterprise (Annually) 2020")] [Display(Name = "Enterprise (Annually) 2020")]
EnterpriseAnnually2020 = 11, 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)")] [Display(Name = "Teams (Monthly)")]
TeamsMonthly = 12, TeamsMonthly = 17,
[Display(Name = "Teams (Annually)")] [Display(Name = "Teams (Annually)")]
TeamsAnnually = 13, TeamsAnnually = 18,
[Display(Name = "Enterprise (Monthly)")] [Display(Name = "Enterprise (Monthly)")]
EnterpriseMonthly = 14, EnterpriseMonthly = 19,
[Display(Name = "Enterprise (Annually)")] [Display(Name = "Enterprise (Annually)")]
EnterpriseAnnually = 15, EnterpriseAnnually = 20,
[Display(Name = "Teams Starter")] [Display(Name = "Teams Starter")]
TeamsStarter = 16, TeamsStarter = 21,
} }

View File

@ -6,7 +6,7 @@
<td class="content-block" <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;" 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"> 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> </td>
</tr> </tr>
<tr <tr

View File

@ -1,5 +1,5 @@
{{#>BasicTextLayout}} {{#>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 For more information, please refer to the following help article: https://bitwarden.com/help/managing-users
{{/BasicTextLayout}} {{/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 System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
namespace Bit.Core.Models.Api; namespace Bit.Core.Models.Api;
@ -7,14 +8,14 @@ public class PushUpdateRequestModel
public 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; OrganizationId = organizationId;
} }
[Required] [Required]
public IEnumerable<string> DeviceIds { get; set; } public IEnumerable<PushDeviceRequestModel> Devices { get; set; }
[Required] [Required]
public string OrganizationId { get; set; } public string OrganizationId { get; set; }
} }

View File

@ -2,7 +2,7 @@
namespace Bit.Core.Models.StaticStore.Plans; namespace Bit.Core.Models.StaticStore.Plans;
public record EnterprisePlan : Models.StaticStore.Plan public record EnterprisePlan : Plan
{ {
public EnterprisePlan(bool isAnnual) public EnterprisePlan(bool isAnnual)
{ {
@ -44,7 +44,7 @@ public record EnterprisePlan : Models.StaticStore.Plan
{ {
BaseSeats = 0; BaseSeats = 0;
BasePrice = 0; BasePrice = 0;
BaseServiceAccount = 200; BaseServiceAccount = 50;
HasAdditionalSeatsOption = true; HasAdditionalSeatsOption = true;
HasAdditionalServiceAccountOption = true; HasAdditionalServiceAccountOption = true;
@ -55,16 +55,16 @@ public record EnterprisePlan : Models.StaticStore.Plan
if (isAnnual) if (isAnnual)
{ {
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually"; StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
StripeServiceAccountPlanId = "secrets-manager-service-account-annually"; StripeServiceAccountPlanId = "secrets-manager-service-account-2024-annually";
SeatPrice = 144; SeatPrice = 144;
AdditionalPricePerServiceAccount = 6; AdditionalPricePerServiceAccount = 12;
} }
else else
{ {
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly"; StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly"; StripeServiceAccountPlanId = "secrets-manager-service-account-2024-monthly";
SeatPrice = 13; 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; namespace Bit.Core.Models.StaticStore.Plans;
public record TeamsPlan : Models.StaticStore.Plan public record TeamsPlan : Plan
{ {
public TeamsPlan(bool isAnnual) public TeamsPlan(bool isAnnual)
{ {
@ -37,7 +37,7 @@ public record TeamsPlan : Models.StaticStore.Plan
{ {
BaseSeats = 0; BaseSeats = 0;
BasePrice = 0; BasePrice = 0;
BaseServiceAccount = 50; BaseServiceAccount = 20;
HasAdditionalSeatsOption = true; HasAdditionalSeatsOption = true;
HasAdditionalServiceAccountOption = true; HasAdditionalServiceAccountOption = true;
@ -48,16 +48,16 @@ public record TeamsPlan : Models.StaticStore.Plan
if (isAnnual) if (isAnnual)
{ {
StripeSeatPlanId = "secrets-manager-teams-seat-annually"; StripeSeatPlanId = "secrets-manager-teams-seat-annually";
StripeServiceAccountPlanId = "secrets-manager-service-account-annually"; StripeServiceAccountPlanId = "secrets-manager-service-account-2024-annually";
SeatPrice = 72; SeatPrice = 72;
AdditionalPricePerServiceAccount = 6; AdditionalPricePerServiceAccount = 12;
} }
else else
{ {
StripeSeatPlanId = "secrets-manager-teams-seat-monthly"; StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly"; StripeServiceAccountPlanId = "secrets-manager-service-account-2024-monthly";
SeatPrice = 7; 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; BaseSeats = 0;
BasePrice = 0; BasePrice = 0;
BaseServiceAccount = 50; BaseServiceAccount = 20;
HasAdditionalSeatsOption = true; HasAdditionalSeatsOption = true;
HasAdditionalServiceAccountOption = true; HasAdditionalServiceAccountOption = true;
@ -45,9 +45,9 @@ public record TeamsStarterPlan : Plan
AllowServiceAccountsAutoscale = true; AllowServiceAccountsAutoscale = true;
StripeSeatPlanId = "secrets-manager-teams-seat-monthly"; StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly"; StripeServiceAccountPlanId = "secrets-manager-service-account-2024-monthly";
SeatPrice = 7; 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) 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 // Check if the organization has unlimited service accounts
if (organization.SmServiceAccounts == null) 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) 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 // Check plan maximum service accounts
@ -267,7 +267,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
{ {
var planMaxServiceAccounts = plan.SecretsManager.BaseServiceAccount + var planMaxServiceAccounts = plan.SecretsManager.BaseServiceAccount +
plan.SecretsManager.MaxAdditionalServiceAccount.GetValueOrDefault(); 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 // Check autoscale maximum service accounts
@ -275,21 +275,21 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
update.SmServiceAccounts.Value > update.MaxAutoscaleSmServiceAccounts.Value) update.SmServiceAccounts.Value > update.MaxAutoscaleSmServiceAccounts.Value)
{ {
var message = update.Autoscaling var message = update.Autoscaling
? "Secrets Manager service account limit has been reached." ? "Secrets Manager machine account limit has been reached."
: "Cannot set max service accounts autoscaling below service account amount."; : "Cannot set max machine accounts autoscaling below machine account amount.";
throw new BadRequestException(message); throw new BadRequestException(message);
} }
// Check minimum service accounts included with plan // Check minimum service accounts included with plan
if (plan.SecretsManager.BaseServiceAccount > update.SmServiceAccounts.Value) 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 // Check minimum service accounts required by business logic
if (update.SmServiceAccounts.Value <= 0) 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 // 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); var currentServiceAccounts = await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id);
if (currentServiceAccounts > update.SmServiceAccounts) if (currentServiceAccounts > update.SmServiceAccounts)
{ {
throw new BadRequestException($"Your organization currently has {currentServiceAccounts} service accounts. " + throw new BadRequestException($"Your organization currently has {currentServiceAccounts} machine accounts. " +
$"You cannot decrease your subscription below your current service account usage."); $"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) if (update.SmServiceAccounts.HasValue && update.MaxAutoscaleSmServiceAccounts.Value < update.SmServiceAccounts.Value)
{ {
throw new BadRequestException( 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) 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) if (plan.SecretsManager.MaxServiceAccounts.HasValue && update.MaxAutoscaleSmServiceAccounts.Value > plan.SecretsManager.MaxServiceAccounts)
{ {
throw new BadRequestException(string.Concat( 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}.", $"but you have specified a max autoscale count of {update.MaxAutoscaleSmServiceAccounts}.",
"Reduce your max autoscale count.")); "Reduce your max autoscale count."));
} }

View File

@ -330,9 +330,9 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
if (currentServiceAccounts > newPlanServiceAccounts) if (currentServiceAccounts > newPlanServiceAccounts)
{ {
throw new BadRequestException( throw new BadRequestException(
$"Your organization currently has {currentServiceAccounts} service accounts. " + $"Your organization currently has {currentServiceAccounts} machine accounts. " +
$"Your new plan only allows {newSecretsManagerPlan.SecretsManager.MaxServiceAccounts} service accounts. " + $"Your new plan only allows {newSecretsManagerPlan.SecretsManager.MaxServiceAccounts} machine accounts. " +
"Remove some service accounts or increase your subscription."); "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, Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
string identifier, DeviceType type); string identifier, DeviceType type);
Task DeleteRegistrationAsync(string deviceId); Task DeleteRegistrationAsync(string deviceId, DeviceType type);
Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId); Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId);
Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, 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) public async Task ClearTokenAsync(Device device)
{ {
await _deviceRepository.ClearPushTokenAsync(device.Id); 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) public async Task DeleteAsync(Device device)
{ {
await _deviceRepository.DeleteAsync(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, public async Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,

View File

@ -951,7 +951,7 @@ public class HandlebarsMailService : IMailService
public async Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount, public async Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount,
IEnumerable<string> ownerEmails) 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 var model = new OrganizationServiceAccountsMaxReachedViewModel
{ {
OrganizationId = organization.Id, OrganizationId = organization.Id,

View File

@ -43,7 +43,8 @@ public class MultiServicePushNotificationService : IPushNotificationService
} }
else 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, _services.Add(new NotificationHubPushNotificationService(installationDeviceRepository,
globalSettings, httpContextAccessor, hubLogger)); globalSettings, httpContextAccessor, hubLogger));

View File

@ -20,8 +20,9 @@ public class NotificationHubPushNotificationService : IPushNotificationService
private readonly IInstallationDeviceRepository _installationDeviceRepository; private readonly IInstallationDeviceRepository _installationDeviceRepository;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHttpContextAccessor _httpContextAccessor;
private NotificationHubClient _client = null; private readonly List<NotificationHubClient> _clients = [];
private ILogger _logger; private readonly bool _enableTracing = false;
private readonly ILogger _logger;
public NotificationHubPushNotificationService( public NotificationHubPushNotificationService(
IInstallationDeviceRepository installationDeviceRepository, IInstallationDeviceRepository installationDeviceRepository,
@ -32,10 +33,18 @@ public class NotificationHubPushNotificationService : IPushNotificationService
_installationDeviceRepository = installationDeviceRepository; _installationDeviceRepository = installationDeviceRepository;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
_client = NotificationHubClient.CreateClientFromConnectionString(
_globalSettings.NotificationHub.ConnectionString, foreach (var hub in globalSettings.NotificationHubs)
_globalSettings.NotificationHub.HubName, {
_globalSettings.NotificationHub.EnableSendTracing); var client = NotificationHubClient.CreateClientFromConnectionString(
hub.ConnectionString,
hub.HubName,
hub.EnableSendTracing);
_clients.Add(client);
_enableTracing = _enableTracing || hub.EnableSendTracing;
}
_logger = logger; _logger = logger;
} }
@ -255,16 +264,31 @@ public class NotificationHubPushNotificationService : IPushNotificationService
private async Task SendPayloadAsync(string tag, PushType type, object payload) private async Task SendPayloadAsync(string tag, PushType type, object payload)
{ {
var outcome = await _client.SendTemplateNotificationAsync( var tasks = new List<Task<NotificationOutcome>>();
new Dictionary<string, string> foreach (var client in _clients)
{
{ "type", ((byte)type).ToString() },
{ "payload", JsonSerializer.Serialize(payload) }
}, tag);
if (_globalSettings.NotificationHub.EnableSendTracing)
{ {
_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}", var task = client.SendTemplateNotificationAsync(
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results); 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.Repositories;
using Bit.Core.Settings; using Bit.Core.Settings;
using Microsoft.Azure.NotificationHubs; using Microsoft.Azure.NotificationHubs;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Services; namespace Bit.Core.Services;
@ -10,18 +11,36 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
{ {
private readonly IInstallationDeviceRepository _installationDeviceRepository; private readonly IInstallationDeviceRepository _installationDeviceRepository;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly ILogger<NotificationHubPushRegistrationService> _logger;
private NotificationHubClient _client = null; private Dictionary<NotificationHubType, NotificationHubClient> _clients = [];
public NotificationHubPushRegistrationService( public NotificationHubPushRegistrationService(
IInstallationDeviceRepository installationDeviceRepository, IInstallationDeviceRepository installationDeviceRepository,
GlobalSettings globalSettings) GlobalSettings globalSettings,
ILogger<NotificationHubPushRegistrationService> logger)
{ {
_installationDeviceRepository = installationDeviceRepository; _installationDeviceRepository = installationDeviceRepository;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_client = NotificationHubClient.CreateClientFromConnectionString( _logger = logger;
_globalSettings.NotificationHub.ConnectionString,
_globalSettings.NotificationHub.HubName); // 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, public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
@ -84,7 +103,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate, BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate,
userId, identifier); userId, identifier);
await _client.CreateOrUpdateInstallationAsync(installation); await GetClient(type).CreateOrUpdateInstallationAsync(installation);
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId)) if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{ {
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId)); await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
@ -119,11 +138,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
installation.Templates.Add(fullTemplateId, template); installation.Templates.Add(fullTemplateId, template);
} }
public async Task DeleteRegistrationAsync(string deviceId) public async Task DeleteRegistrationAsync(string deviceId, DeviceType deviceType)
{ {
try try
{ {
await _client.DeleteInstallationAsync(deviceId); await GetClient(deviceType).DeleteInstallationAsync(deviceId);
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId)) if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{ {
await _installationDeviceRepository.DeleteAsync(new InstallationDeviceEntity(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}"); await PatchTagsForUserDevicesAsync(devices, UpdateOperationType.Add, $"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()); 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}"); $"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()); 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) string tag)
{ {
if (!deviceIds.Any()) if (!devices.Any())
{ {
return; return;
} }
@ -179,11 +198,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
operation.Path += $"/{tag}"; operation.Path += $"/{tag}";
} }
foreach (var id in deviceIds) foreach (var device in devices)
{ {
try 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")) 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); 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; return;
} }
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId); var requestModel = new PushUpdateRequestModel(devices, organizationId);
await SendAsync(HttpMethod.Put, "push/add-organization", requestModel); 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; return;
} }
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId); var requestModel = new PushUpdateRequestModel(devices, organizationId);
await SendAsync(HttpMethod.Put, "push/delete-organization", requestModel); await SendAsync(HttpMethod.Put, "push/delete-organization", requestModel);
} }
} }

View File

@ -4,7 +4,7 @@ namespace Bit.Core.Services;
public class NoopPushRegistrationService : IPushRegistrationService 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); return Task.FromResult(0);
} }
@ -15,12 +15,12 @@ public class NoopPushRegistrationService : IPushRegistrationService
return Task.FromResult(0); return Task.FromResult(0);
} }
public Task DeleteRegistrationAsync(string deviceId) public Task DeleteRegistrationAsync(string deviceId, DeviceType deviceType)
{ {
return Task.FromResult(0); 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); return Task.FromResult(0);
} }

View File

@ -1,4 +1,5 @@
using Bit.Core.Auth.Settings; using Bit.Core.Auth.Settings;
using Bit.Core.Enums;
using Bit.Core.Settings.LoggingSettings; using Bit.Core.Settings.LoggingSettings;
namespace Bit.Core.Settings; namespace Bit.Core.Settings;
@ -64,7 +65,7 @@ public class GlobalSettings : IGlobalSettings
public virtual SentrySettings Sentry { get; set; } = new SentrySettings(); public virtual SentrySettings Sentry { get; set; } = new SentrySettings();
public virtual SyslogSettings Syslog { get; set; } = new SyslogSettings(); public virtual SyslogSettings Syslog { get; set; } = new SyslogSettings();
public virtual ILogLevelSettings MinLogLevel { get; set; } = new LogLevelSettings(); 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 YubicoSettings Yubico { get; set; } = new YubicoSettings();
public virtual DuoSettings Duo { get; set; } = new DuoSettings(); public virtual DuoSettings Duo { get; set; } = new DuoSettings();
public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings(); public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings();
@ -414,12 +415,16 @@ public class GlobalSettings : IGlobalSettings
set => _connectionString = value.Trim('"'); set => _connectionString = value.Trim('"');
} }
public string HubName { get; set; } public string HubName { get; set; }
/// <summary> /// <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). /// 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. /// 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> /// </summary>
public bool EnableSendTracing { get; set; } = false; 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 public class YubicoSettings

View File

@ -114,8 +114,13 @@ public static class StaticStore
new TeamsPlan(true), new TeamsPlan(true),
new TeamsPlan(false), new TeamsPlan(false),
new Enterprise2023Plan(true),
new Enterprise2023Plan(false),
new Enterprise2020Plan(true), new Enterprise2020Plan(true),
new Enterprise2020Plan(false), new Enterprise2020Plan(false),
new TeamsStarterPlan2023(),
new Teams2023Plan(true),
new Teams2023Plan(false),
new Teams2020Plan(true), new Teams2020Plan(true),
new Teams2020Plan(false), new Teams2020Plan(false),
new FamiliesPlan(), new FamiliesPlan(),

View File

@ -26,7 +26,20 @@ public class OrganizationMapperProfile : Profile
{ {
public OrganizationMapperProfile() 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>() CreateProjection<Organization, SelfHostedOrganizationDetails>()
.ForMember(sd => sd.CollectionCount, opt => opt.MapFrom(o => o.Collections.Count)) .ForMember(sd => sd.CollectionCount, opt => opt.MapFrom(o => o.Collections.Count))
.ForMember(sd => sd.GroupCount, opt => opt.MapFrom(o => o.Groups.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 dbContext = GetDatabaseContext(scope);
var organizations = await GetDbSet(dbContext) var organizations = await GetDbSet(dbContext)
.Select(e => e.OrganizationUsers .SelectMany(e => e.OrganizationUsers
.Where(ou => ou.UserId == userId) .Where(ou => ou.UserId == userId))
.Select(ou => ou.Organization)) .Include(ou => ou.Organization)
.Select(ou => ou.Organization)
.ToListAsync(); .ToListAsync();
return Mapper.Map<List<Core.AdminConsole.Entities.Organization>>(organizations); 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;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
using NSubstitute.ExceptionExtensions;
using NSubstitute.ReturnsExtensions; using NSubstitute.ReturnsExtensions;
using Xunit; using Xunit;
@ -25,9 +24,6 @@ public class CreateOrganizationDomainCommandTests
sutProvider.GetDependency<IOrganizationDomainRepository>() sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetDomainByOrgIdAndDomainNameAsync(orgDomain.OrganizationId, orgDomain.DomainName) .GetDomainByOrgIdAndDomainNameAsync(orgDomain.OrganizationId, orgDomain.DomainName)
.ReturnsNull(); .ReturnsNull();
sutProvider.GetDependency<IDnsResolverService>()
.ResolveAsync(orgDomain.DomainName, orgDomain.Txt)
.Returns(false);
orgDomain.SetNextRunDate(12); orgDomain.SetNextRunDate(12);
sutProvider.GetDependency<IOrganizationDomainRepository>() sutProvider.GetDependency<IOrganizationDomainRepository>()
.CreateAsync(orgDomain) .CreateAsync(orgDomain)
@ -38,12 +34,12 @@ public class CreateOrganizationDomainCommandTests
Assert.Equal(orgDomain.Id, result.Id); Assert.Equal(orgDomain.Id, result.Id);
Assert.Equal(orgDomain.OrganizationId, result.OrganizationId); 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); Assert.Equal(orgDomain.NextRunDate, result.NextRunDate);
await sutProvider.GetDependency<IEventService>().Received(1) await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationDomainEventAsync(Arg.Any<OrganizationDomain>(), EventType.OrganizationDomain_Added); .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] [Theory, BitAutoData]
@ -79,52 +75,4 @@ public class CreateOrganizationDomainCommandTests
var exception = await Assert.ThrowsAsync<ConflictException>(requestAction); var exception = await Assert.ThrowsAsync<ConflictException>(requestAction);
Assert.Contains("A domain already exists for this organization.", exception.Message); 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>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpAsync(signup)); () => 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] [Theory]
@ -444,7 +444,7 @@ public class OrganizationServiceTests
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpAsync(signup)); () => 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] [Theory]
@ -2208,7 +2208,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
AdditionalSeats = 3 AdditionalSeats = 3
}; };
var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup)); 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] [Theory]
@ -2249,7 +2249,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
AdditionalSeats = 5 AdditionalSeats = 5
}; };
var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup)); 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] [Theory]

View File

@ -447,7 +447,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustServiceAccounts(1); var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustServiceAccounts(1);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); 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); await VerifyDependencyNotCalledAsync(sutProvider);
} }
@ -460,7 +460,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustServiceAccounts(-2); var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustServiceAccounts(-2);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); 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); await VerifyDependencyNotCalledAsync(sutProvider);
} }
@ -475,7 +475,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustServiceAccounts(1); var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustServiceAccounts(1);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); 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); exception.Message, StringComparison.InvariantCultureIgnoreCase);
await VerifyDependencyNotCalledAsync(sutProvider); await VerifyDependencyNotCalledAsync(sutProvider);
} }
@ -492,7 +492,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustServiceAccounts(2); var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustServiceAccounts(2);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); 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); await VerifyDependencyNotCalledAsync(sutProvider);
} }
@ -516,7 +516,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpdateSubscriptionAsync(update)); () => 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); await VerifyDependencyNotCalledAsync(sutProvider);
} }
@ -526,7 +526,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
Organization organization, Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
const int newSmServiceAccounts = 199; const int newSmServiceAccounts = 49;
organization.SmServiceAccounts = newSmServiceAccounts - 10; organization.SmServiceAccounts = newSmServiceAccounts - 10;
@ -537,7 +537,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpdateSubscriptionAsync(update)); () => 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); await VerifyDependencyNotCalledAsync(sutProvider);
} }
@ -570,7 +570,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
.Returns(currentServiceAccounts); .Returns(currentServiceAccounts);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); 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); await VerifyDependencyNotCalledAsync(sutProvider);
} }
@ -648,7 +648,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var update = new SecretsManagerSubscriptionUpdate(organization, false) { MaxAutoscaleSmServiceAccounts = 3 }; var update = new SecretsManagerSubscriptionUpdate(organization, false) { MaxAutoscaleSmServiceAccounts = 3 };
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); 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); await VerifyDependencyNotCalledAsync(sutProvider);
} }

View File

@ -192,7 +192,7 @@ public class UpgradeOrganizationPlanCommandTests
.GetServiceAccountCountByOrganizationIdAsync(organization.Id).Returns(currentServiceAccounts); .GetServiceAccountCountByOrganizationIdAsync(organization.Id).Returns(currentServiceAccounts);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade)); 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); sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs().ReplaceAndUpdateCacheAsync(default);
} }

View File

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

View File

@ -13,7 +13,7 @@ public class StaticStoreTests
var plans = StaticStore.Plans.ToList(); var plans = StaticStore.Plans.ToList();
Assert.NotNull(plans); Assert.NotNull(plans);
Assert.NotEmpty(plans); Assert.NotEmpty(plans);
Assert.Equal(17, plans.Count); Assert.Equal(22, plans.Count);
} }
[Theory] [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.Enums;
using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations;
using Bit.Core.Test.AutoFixture.Attributes; using Bit.Core.Test.AutoFixture.Attributes;
using Bit.Infrastructure.EFIntegration.Test.AutoFixture; using Bit.Infrastructure.EFIntegration.Test.AutoFixture;
using Bit.Infrastructure.EFIntegration.Test.Repositories.EqualityComparers; using Bit.Infrastructure.EFIntegration.Test.Repositories.EqualityComparers;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
using Xunit; using Xunit;
using EfRepo = Bit.Infrastructure.EntityFramework.Repositories; using EfRepo = Bit.Infrastructure.EntityFramework.Repositories;
using Organization = Bit.Core.AdminConsole.Entities.Organization; using Organization = Bit.Core.AdminConsole.Entities.Organization;
@ -13,6 +15,13 @@ namespace Bit.Infrastructure.EFIntegration.Test.Repositories;
public class OrganizationRepositoryTests public class OrganizationRepositoryTests
{ {
[Fact]
public void ValidateOrganizationMappings_ReturnsSuccess()
{
var config = new MapperConfiguration(cfg => cfg.AddProfile<OrganizationMapperProfile>());
config.AssertConfigurationIsValid();
}
[CiSkippedTheory, EfOrganizationAutoData] [CiSkippedTheory, EfOrganizationAutoData]
public async Task CreateAsync_Works_DataMatches( public async Task CreateAsync_Works_DataMatches(
Organization organization, Organization organization,