mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05:00
Merge branch 'ac/ac-2323/sql-automatic-data-migrations' into ac/ac-1682/ef-migrations
# Conflicts: # src/Core/Constants.cs
This commit is contained in:
5
.github/workflows/scan.yml
vendored
5
.github/workflows/scan.yml
vendored
@ -40,7 +40,10 @@ jobs:
|
||||
base_uri: https://ast.checkmarx.net/
|
||||
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
|
||||
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
|
||||
additional_params: --report-format sarif --output-path . ${{ env.INCREMENTAL }}
|
||||
additional_params: |
|
||||
--report-format sarif \
|
||||
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
|
||||
--output-path . ${{ env.INCREMENTAL }}
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
|
||||
|
@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2024.3.1</Version>
|
||||
<Version>2024.4.0</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
@ -1,10 +1,15 @@
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Commercial.Core.AdminConsole.Providers;
|
||||
|
||||
@ -14,21 +19,28 @@ public class CreateProviderCommand : ICreateProviderCommand
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IProviderPlanRepository _providerPlanRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public CreateProviderCommand(
|
||||
IProviderRepository providerRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IProviderService providerService,
|
||||
IUserRepository userRepository)
|
||||
IUserRepository userRepository,
|
||||
IProviderPlanRepository providerPlanRepository,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_providerRepository = providerRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_providerService = providerService;
|
||||
_userRepository = userRepository;
|
||||
_providerPlanRepository = providerPlanRepository;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
public async Task CreateMspAsync(Provider provider, string ownerEmail)
|
||||
public async Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats)
|
||||
{
|
||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
|
||||
if (owner == null)
|
||||
{
|
||||
@ -44,8 +56,24 @@ public class CreateProviderCommand : ICreateProviderCommand
|
||||
Type = ProviderUserType.ProviderAdmin,
|
||||
Status = ProviderUserStatusType.Confirmed,
|
||||
};
|
||||
|
||||
if (isConsolidatedBillingEnabled)
|
||||
{
|
||||
var providerPlans = new List<ProviderPlan>
|
||||
{
|
||||
CreateProviderPlan(provider.Id, PlanType.TeamsMonthly, teamsMinimumSeats),
|
||||
CreateProviderPlan(provider.Id, PlanType.EnterpriseMonthly, enterpriseMinimumSeats)
|
||||
};
|
||||
|
||||
foreach (var providerPlan in providerPlans)
|
||||
{
|
||||
await _providerPlanRepository.CreateAsync(providerPlan);
|
||||
}
|
||||
}
|
||||
|
||||
await _providerUserRepository.CreateAsync(providerUser);
|
||||
await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
||||
|
||||
}
|
||||
|
||||
public async Task CreateResellerAsync(Provider provider)
|
||||
@ -60,4 +88,16 @@ public class CreateProviderCommand : ICreateProviderCommand
|
||||
provider.UseEvents = true;
|
||||
await _providerRepository.CreateAsync(provider);
|
||||
}
|
||||
|
||||
private ProviderPlan CreateProviderPlan(Guid providerId, PlanType planType, int seatMinimum)
|
||||
{
|
||||
return new ProviderPlan
|
||||
{
|
||||
ProviderId = providerId,
|
||||
PlanType = planType,
|
||||
SeatMinimum = seatMinimum,
|
||||
PurchasedSeats = 0,
|
||||
AllocatedSeats = 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -382,10 +382,14 @@ public class ProviderService : IProviderService
|
||||
|
||||
organization.BillingEmail = provider.BillingEmail;
|
||||
await _organizationRepository.ReplaceAsync(organization);
|
||||
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
||||
|
||||
if (!string.IsNullOrEmpty(organization.GatewayCustomerId))
|
||||
{
|
||||
Email = provider.BillingEmail
|
||||
});
|
||||
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
||||
{
|
||||
Email = provider.BillingEmail
|
||||
});
|
||||
}
|
||||
|
||||
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added);
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ public class CreateProviderCommandTests
|
||||
provider.Type = ProviderType.Msp;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.CreateMspAsync(provider, default));
|
||||
() => sutProvider.Sut.CreateMspAsync(provider, default, default, default));
|
||||
Assert.Contains("Invalid owner.", exception.Message);
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ public class CreateProviderCommandTests
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
userRepository.GetByEmailAsync(user.Email).Returns(user);
|
||||
|
||||
await sutProvider.Sut.CreateMspAsync(provider, user.Email);
|
||||
await sutProvider.Sut.CreateMspAsync(provider, user.Email, default, default);
|
||||
|
||||
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
||||
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);
|
||||
|
@ -8,6 +8,8 @@ using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@ -34,6 +36,7 @@ public class ProvidersController : Controller
|
||||
private readonly IUserService _userService;
|
||||
private readonly ICreateProviderCommand _createProviderCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IProviderPlanRepository _providerPlanRepository;
|
||||
|
||||
public ProvidersController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -47,7 +50,8 @@ public class ProvidersController : Controller
|
||||
IReferenceEventService referenceEventService,
|
||||
IUserService userService,
|
||||
ICreateProviderCommand createProviderCommand,
|
||||
IFeatureService featureService)
|
||||
IFeatureService featureService,
|
||||
IProviderPlanRepository providerPlanRepository)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationService = organizationService;
|
||||
@ -61,6 +65,7 @@ public class ProvidersController : Controller
|
||||
_userService = userService;
|
||||
_createProviderCommand = createProviderCommand;
|
||||
_featureService = featureService;
|
||||
_providerPlanRepository = providerPlanRepository;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Provider_List_View)]
|
||||
@ -90,11 +95,13 @@ public class ProvidersController : Controller
|
||||
});
|
||||
}
|
||||
|
||||
public IActionResult Create(string ownerEmail = null)
|
||||
public IActionResult Create(int teamsMinimumSeats, int enterpriseMinimumSeats, string ownerEmail = null)
|
||||
{
|
||||
return View(new CreateProviderModel
|
||||
{
|
||||
OwnerEmail = ownerEmail
|
||||
OwnerEmail = ownerEmail,
|
||||
TeamsMinimumSeats = teamsMinimumSeats,
|
||||
EnterpriseMinimumSeats = enterpriseMinimumSeats
|
||||
});
|
||||
}
|
||||
|
||||
@ -112,7 +119,8 @@ public class ProvidersController : Controller
|
||||
switch (provider.Type)
|
||||
{
|
||||
case ProviderType.Msp:
|
||||
await _createProviderCommand.CreateMspAsync(provider, model.OwnerEmail);
|
||||
await _createProviderCommand.CreateMspAsync(provider, model.OwnerEmail, model.TeamsMinimumSeats,
|
||||
model.EnterpriseMinimumSeats);
|
||||
break;
|
||||
case ProviderType.Reseller:
|
||||
await _createProviderCommand.CreateResellerAsync(provider);
|
||||
@ -139,6 +147,7 @@ public class ProvidersController : Controller
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IActionResult> Edit(Guid id)
|
||||
{
|
||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
if (provider == null)
|
||||
{
|
||||
@ -147,7 +156,12 @@ public class ProvidersController : Controller
|
||||
|
||||
var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);
|
||||
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
|
||||
return View(new ProviderEditModel(provider, users, providerOrganizations));
|
||||
if (isConsolidatedBillingEnabled)
|
||||
{
|
||||
var providerPlan = await _providerPlanRepository.GetByProviderId(id);
|
||||
return View(new ProviderEditModel(provider, users, providerOrganizations, providerPlan));
|
||||
}
|
||||
return View(new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>()));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -156,6 +170,8 @@ public class ProvidersController : Controller
|
||||
[RequirePermission(Permission.Provider_Edit)]
|
||||
public async Task<IActionResult> Edit(Guid id, ProviderEditModel model)
|
||||
{
|
||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
if (provider == null)
|
||||
{
|
||||
@ -165,6 +181,15 @@ public class ProvidersController : Controller
|
||||
model.ToProvider(provider);
|
||||
await _providerRepository.ReplaceAsync(provider);
|
||||
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
|
||||
if (isConsolidatedBillingEnabled)
|
||||
{
|
||||
model.ToProviderPlan(providerPlans);
|
||||
foreach (var providerPlan in providerPlans)
|
||||
{
|
||||
await _providerPlanRepository.ReplaceAsync(providerPlan);
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,12 @@ public class CreateProviderModel : IValidatableObject
|
||||
[Display(Name = "Primary Billing Email")]
|
||||
public string BillingEmail { get; set; }
|
||||
|
||||
[Display(Name = "Teams minimum seats")]
|
||||
public int TeamsMinimumSeats { get; set; }
|
||||
|
||||
[Display(Name = "Enterprise minimum seats")]
|
||||
public int EnterpriseMinimumSeats { get; set; }
|
||||
|
||||
public virtual Provider ToProvider()
|
||||
{
|
||||
return new Provider()
|
||||
@ -45,6 +51,16 @@ public class CreateProviderModel : IValidatableObject
|
||||
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(OwnerEmail);
|
||||
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
|
||||
}
|
||||
if (TeamsMinimumSeats < 0)
|
||||
{
|
||||
var teamsMinimumSeatsDisplayName = nameof(TeamsMinimumSeats).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(TeamsMinimumSeats);
|
||||
yield return new ValidationResult($"The {teamsMinimumSeatsDisplayName} field can not be negative.");
|
||||
}
|
||||
if (EnterpriseMinimumSeats < 0)
|
||||
{
|
||||
var enterpriseMinimumSeatsDisplayName = nameof(EnterpriseMinimumSeats).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMinimumSeats);
|
||||
yield return new ValidationResult($"The {enterpriseMinimumSeatsDisplayName} field can not be negative.");
|
||||
}
|
||||
break;
|
||||
case ProviderType.Reseller:
|
||||
if (string.IsNullOrWhiteSpace(Name))
|
||||
|
@ -1,6 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
@ -8,13 +10,16 @@ public class ProviderEditModel : ProviderViewModel
|
||||
{
|
||||
public ProviderEditModel() { }
|
||||
|
||||
public ProviderEditModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers, IEnumerable<ProviderOrganizationOrganizationDetails> organizations)
|
||||
public ProviderEditModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers,
|
||||
IEnumerable<ProviderOrganizationOrganizationDetails> organizations, IEnumerable<ProviderPlan> providerPlans)
|
||||
: base(provider, providerUsers, organizations)
|
||||
{
|
||||
Name = provider.DisplayName();
|
||||
BusinessName = provider.DisplayBusinessName();
|
||||
BillingEmail = provider.BillingEmail;
|
||||
BillingPhone = provider.BillingPhone;
|
||||
TeamsMinimumSeats = GetMinimumSeats(providerPlans, PlanType.TeamsMonthly);
|
||||
EnterpriseMinimumSeats = GetMinimumSeats(providerPlans, PlanType.EnterpriseMonthly);
|
||||
}
|
||||
|
||||
[Display(Name = "Billing Email")]
|
||||
@ -24,12 +29,38 @@ public class ProviderEditModel : ProviderViewModel
|
||||
[Display(Name = "Business Name")]
|
||||
public string BusinessName { get; set; }
|
||||
public string Name { get; set; }
|
||||
[Display(Name = "Teams minimum seats")]
|
||||
public int TeamsMinimumSeats { get; set; }
|
||||
|
||||
[Display(Name = "Enterprise minimum seats")]
|
||||
public int EnterpriseMinimumSeats { get; set; }
|
||||
[Display(Name = "Events")]
|
||||
|
||||
public IEnumerable<ProviderPlan> ToProviderPlan(IEnumerable<ProviderPlan> existingProviderPlans)
|
||||
{
|
||||
var providerPlans = existingProviderPlans.ToList();
|
||||
foreach (var existingProviderPlan in providerPlans)
|
||||
{
|
||||
existingProviderPlan.SeatMinimum = existingProviderPlan.PlanType switch
|
||||
{
|
||||
PlanType.TeamsMonthly => TeamsMinimumSeats,
|
||||
PlanType.EnterpriseMonthly => EnterpriseMinimumSeats,
|
||||
_ => existingProviderPlan.SeatMinimum
|
||||
};
|
||||
}
|
||||
return providerPlans;
|
||||
}
|
||||
|
||||
public Provider ToProvider(Provider existingProvider)
|
||||
{
|
||||
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
|
||||
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant()?.Trim();
|
||||
return existingProvider;
|
||||
}
|
||||
|
||||
|
||||
private int GetMinimumSeats(IEnumerable<ProviderPlan> providerPlans, PlanType planType)
|
||||
{
|
||||
return (from providerPlan in providerPlans where providerPlan.PlanType == planType select (int)providerPlan.SeatMinimum).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
@using Bit.SharedWeb.Utilities
|
||||
@using Bit.Core.AdminConsole.Enums.Provider
|
||||
@using Bit.Core
|
||||
@model CreateProviderModel
|
||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||
@{
|
||||
ViewData["Title"] = "Create Provider";
|
||||
}
|
||||
@ -39,6 +41,23 @@
|
||||
<label asp-for="OwnerEmail"></label>
|
||||
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||
</div>
|
||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="TeamsMinimumSeats"></label>
|
||||
<input type="number" class="form-control" asp-for="TeamsMinimumSeats">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="EnterpriseMinimumSeats"></label>
|
||||
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div id="@($"info-{(int)ProviderType.Reseller}")" class="form-group @(Model.Type != ProviderType.Reseller ? "d-none" : string.Empty)">
|
||||
|
@ -1,5 +1,7 @@
|
||||
@using Bit.Admin.Enums;
|
||||
@using Bit.Core
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||
|
||||
@model ProviderEditModel
|
||||
@{
|
||||
@ -41,6 +43,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="TeamsMinimumSeats"></label>
|
||||
<input type="number" class="form-control" asp-for="TeamsMinimumSeats">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="EnterpriseMinimumSeats"></label>
|
||||
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
@await Html.PartialAsync("Organizations", Model)
|
||||
@if (canEdit)
|
||||
|
@ -80,7 +80,6 @@ public class OrganizationDomainController : Controller
|
||||
var organizationDomain = new OrganizationDomain
|
||||
{
|
||||
OrganizationId = orgId,
|
||||
Txt = model.Txt,
|
||||
DomainName = model.DomainName.ToLower()
|
||||
};
|
||||
|
||||
|
@ -4,9 +4,6 @@ namespace Bit.Api.AdminConsole.Models.Request;
|
||||
|
||||
public class OrganizationDomainRequestModel
|
||||
{
|
||||
[Required]
|
||||
public string Txt { get; set; }
|
||||
|
||||
[Required]
|
||||
public string DomainName { get; set; }
|
||||
}
|
||||
|
@ -42,11 +42,11 @@ public class PushController : Controller
|
||||
Prefix(model.UserId), Prefix(model.Identifier), model.Type);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task Delete(string id)
|
||||
[HttpPost("delete")]
|
||||
public async Task PostDelete([FromBody] PushDeviceRequestModel model)
|
||||
{
|
||||
CheckUsage();
|
||||
await _pushRegistrationService.DeleteRegistrationAsync(Prefix(id));
|
||||
await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id), model.Type);
|
||||
}
|
||||
|
||||
[HttpPut("add-organization")]
|
||||
@ -54,7 +54,8 @@ public class PushController : Controller
|
||||
{
|
||||
CheckUsage();
|
||||
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(
|
||||
model.DeviceIds.Select(d => Prefix(d)), Prefix(model.OrganizationId));
|
||||
model.Devices.Select(d => new KeyValuePair<string, Core.Enums.DeviceType>(Prefix(d.Id), d.Type)),
|
||||
Prefix(model.OrganizationId));
|
||||
}
|
||||
|
||||
[HttpPut("delete-organization")]
|
||||
@ -62,7 +63,8 @@ public class PushController : Controller
|
||||
{
|
||||
CheckUsage();
|
||||
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(
|
||||
model.DeviceIds.Select(d => Prefix(d)), Prefix(model.OrganizationId));
|
||||
model.Devices.Select(d => new KeyValuePair<string, Core.Enums.DeviceType>(Prefix(d.Id), d.Type)),
|
||||
Prefix(model.OrganizationId));
|
||||
}
|
||||
|
||||
[HttpPost("send")]
|
||||
|
@ -85,7 +85,7 @@ public class PeopleAccessPoliciesRequestModel
|
||||
|
||||
if (!policies.All(ap => ap.Read && ap.Write))
|
||||
{
|
||||
throw new BadRequestException("Service account access must be Can read, write");
|
||||
throw new BadRequestException("Machine account access must be Can read, write");
|
||||
}
|
||||
|
||||
return new ServiceAccountPeopleAccessPolicies
|
||||
|
@ -560,7 +560,7 @@ public class CiphersController : Controller
|
||||
|
||||
[HttpPut("{id}/collections")]
|
||||
[HttpPost("{id}/collections")]
|
||||
public async Task PutCollections(Guid id, [FromBody] CipherCollectionsRequestModel model)
|
||||
public async Task<CipherResponseModel> PutCollections(Guid id, [FromBody] CipherCollectionsRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await GetByIdAsync(id, userId);
|
||||
@ -572,6 +572,10 @@ public class CiphersController : Controller
|
||||
|
||||
await _cipherService.SaveCollectionsAsync(cipher,
|
||||
model.CollectionIds.Select(c => new Guid(c)), userId, false);
|
||||
|
||||
var updatedCipherCollections = await GetByIdAsync(id, userId);
|
||||
var response = new CipherResponseModel(updatedCipherCollections, _globalSettings);
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPut("{id}/collections-admin")]
|
||||
@ -1106,6 +1110,33 @@ public class CiphersController : Controller
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the user is an admin or owner of an organization with unassigned ciphers (i.e. ciphers that
|
||||
/// are not assigned to a collection).
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("has-unassigned-ciphers")]
|
||||
public async Task<bool> HasUnassignedCiphers()
|
||||
{
|
||||
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
|
||||
var adminOrganizations = _currentContext.Organizations
|
||||
.Where(o => o.Type is OrganizationUserType.Admin or OrganizationUserType.Owner &&
|
||||
orgAbilities.ContainsKey(o.Id) && orgAbilities[o.Id].FlexibleCollections);
|
||||
|
||||
foreach (var org in adminOrganizations)
|
||||
{
|
||||
var unassignedCiphers = await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(org.Id);
|
||||
// We only care about non-deleted ciphers
|
||||
if (unassignedCiphers.Any(c => c.DeletedDate == null))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ValidateAttachment()
|
||||
{
|
||||
if (!Request?.ContentType.Contains("multipart/") ?? true)
|
||||
|
@ -75,7 +75,7 @@ public class PayPalController : Controller
|
||||
|
||||
if (string.IsNullOrEmpty(transactionModel.TransactionId))
|
||||
{
|
||||
_logger.LogError("PayPal IPN: Transaction ID is missing");
|
||||
_logger.LogWarning("PayPal IPN: Transaction ID is missing");
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -453,7 +453,7 @@ public class StripeController : Controller
|
||||
}
|
||||
else if (parsedEvent.Type.Equals(HandledStripeWebhook.ChargeRefunded))
|
||||
{
|
||||
var charge = await _stripeEventService.GetCharge(parsedEvent);
|
||||
var charge = await _stripeEventService.GetCharge(parsedEvent, true, ["refunds"]);
|
||||
var chargeTransaction = await _transactionRepository.GetByGatewayIdAsync(
|
||||
GatewayType.Stripe, charge.Id);
|
||||
if (chargeTransaction == null)
|
||||
|
@ -5,6 +5,7 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
@ -50,26 +51,16 @@ public class CreateOrganizationDomainCommand : ICreateOrganizationDomainCommand
|
||||
throw new ConflictException("A domain already exists for this organization.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (await _dnsResolverService.ResolveAsync(organizationDomain.DomainName, organizationDomain.Txt))
|
||||
{
|
||||
organizationDomain.SetVerifiedDate();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error verifying Organization domain.");
|
||||
}
|
||||
|
||||
// Generate and set DNS TXT Record
|
||||
// DNS-Based Service Discovery RFC: https://www.ietf.org/rfc/rfc6763.txt; see section 6.1
|
||||
// Google uses 43 chars for their TXT record value: https://support.google.com/a/answer/2716802
|
||||
// A random 44 character string was used here to keep parity with prior client-side generation of 47 characters
|
||||
organizationDomain.Txt = string.Join("=", "bw", CoreHelpers.RandomString(44));
|
||||
organizationDomain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
|
||||
organizationDomain.SetLastCheckedDate();
|
||||
|
||||
var orgDomain = await _organizationDomainRepository.CreateAsync(organizationDomain);
|
||||
|
||||
await _eventService.LogOrganizationDomainEventAsync(orgDomain, EventType.OrganizationDomain_Added);
|
||||
await _eventService.LogOrganizationDomainEventAsync(orgDomain,
|
||||
orgDomain.VerifiedDate != null ? EventType.OrganizationDomain_Verified : EventType.OrganizationDomain_NotVerified);
|
||||
|
||||
return orgDomain;
|
||||
}
|
||||
|
@ -4,6 +4,6 @@ namespace Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
|
||||
public interface ICreateProviderCommand
|
||||
{
|
||||
Task CreateMspAsync(Provider provider, string ownerEmail);
|
||||
Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats);
|
||||
Task CreateResellerAsync(Provider provider);
|
||||
}
|
||||
|
@ -681,8 +681,8 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
await _organizationUserRepository.CreateAsync(orgUser);
|
||||
|
||||
var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
|
||||
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(deviceIds,
|
||||
var devices = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
|
||||
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(devices,
|
||||
organization.Id.ToString());
|
||||
await _pushNotificationService.PushSyncOrgKeysAsync(ownerId);
|
||||
}
|
||||
@ -1932,17 +1932,19 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
|
||||
{
|
||||
var deviceIds = await GetUserDeviceIdsAsync(userId);
|
||||
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(deviceIds,
|
||||
var devices = await GetUserDeviceIdsAsync(userId);
|
||||
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices,
|
||||
organizationId.ToString());
|
||||
await _pushNotificationService.PushSyncOrgKeysAsync(userId);
|
||||
}
|
||||
|
||||
|
||||
private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
|
||||
private async Task<IEnumerable<KeyValuePair<string, DeviceType>>> GetUserDeviceIdsAsync(Guid userId)
|
||||
{
|
||||
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
|
||||
return devices.Where(d => !string.IsNullOrWhiteSpace(d.PushToken)).Select(d => d.Id.ToString());
|
||||
return devices
|
||||
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
||||
.Select(d => new KeyValuePair<string, DeviceType>(d.Id.ToString(), d.Type));
|
||||
}
|
||||
|
||||
public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null)
|
||||
@ -2037,7 +2039,7 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
if (!plan.SecretsManager.HasAdditionalServiceAccountOption && upgrade.AdditionalServiceAccounts > 0)
|
||||
{
|
||||
throw new BadRequestException("Plan does not allow additional Service Accounts.");
|
||||
throw new BadRequestException("Plan does not allow additional Machine Accounts.");
|
||||
}
|
||||
|
||||
if ((plan.Product == ProductType.TeamsStarter &&
|
||||
@ -2050,7 +2052,7 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
if (upgrade.AdditionalServiceAccounts.GetValueOrDefault() < 0)
|
||||
{
|
||||
throw new BadRequestException("You can't subtract Service Accounts!");
|
||||
throw new BadRequestException("You can't subtract Machine Accounts!");
|
||||
}
|
||||
|
||||
switch (plan.SecretsManager.HasAdditionalSeatsOption)
|
||||
|
@ -131,6 +131,9 @@ public static class FeatureFlagKeys
|
||||
public const string PM5864DollarThreshold = "PM-5864-dollar-threshold";
|
||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||
public const string ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners";
|
||||
public const string EnableConsolidatedBilling = "enable-consolidated-billing";
|
||||
public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section";
|
||||
public const string UnassignedItemsBanner = "unassigned-items-banner";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
|
11
src/Core/Enums/NotificationHubType.cs
Normal file
11
src/Core/Enums/NotificationHubType.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Bit.Core.Enums;
|
||||
|
||||
public enum NotificationHubType
|
||||
{
|
||||
General = 0,
|
||||
Android = 1,
|
||||
iOS = 2,
|
||||
GeneralWeb = 3,
|
||||
GeneralBrowserExtension = 4,
|
||||
GeneralDesktop = 5
|
||||
}
|
@ -28,14 +28,24 @@ public enum PlanType : byte
|
||||
EnterpriseMonthly2020 = 10,
|
||||
[Display(Name = "Enterprise (Annually) 2020")]
|
||||
EnterpriseAnnually2020 = 11,
|
||||
[Display(Name = "Teams (Monthly) 2023")]
|
||||
TeamsMonthly2023 = 12,
|
||||
[Display(Name = "Teams (Annually) 2023")]
|
||||
TeamsAnnually2023 = 13,
|
||||
[Display(Name = "Enterprise (Monthly) 2023")]
|
||||
EnterpriseMonthly2023 = 14,
|
||||
[Display(Name = "Enterprise (Annually) 2023")]
|
||||
EnterpriseAnnually2023 = 15,
|
||||
[Display(Name = "Teams Starter 2023")]
|
||||
TeamsStarter2023 = 16,
|
||||
[Display(Name = "Teams (Monthly)")]
|
||||
TeamsMonthly = 12,
|
||||
TeamsMonthly = 17,
|
||||
[Display(Name = "Teams (Annually)")]
|
||||
TeamsAnnually = 13,
|
||||
TeamsAnnually = 18,
|
||||
[Display(Name = "Enterprise (Monthly)")]
|
||||
EnterpriseMonthly = 14,
|
||||
EnterpriseMonthly = 19,
|
||||
[Display(Name = "Enterprise (Annually)")]
|
||||
EnterpriseAnnually = 15,
|
||||
EnterpriseAnnually = 20,
|
||||
[Display(Name = "Teams Starter")]
|
||||
TeamsStarter = 16,
|
||||
TeamsStarter = 21,
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
<td class="content-block"
|
||||
style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;"
|
||||
valign="top">
|
||||
Your organization has reached the Secrets Manager service accounts limit of {{MaxServiceAccountsCount}}. New service accounts cannot be created
|
||||
Your organization has reached the Secrets Manager machine accounts limit of {{MaxServiceAccountsCount}}. New machine accounts cannot be created
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
|
@ -1,5 +1,5 @@
|
||||
{{#>BasicTextLayout}}
|
||||
Your organization has reached the Secrets Manager service accounts limit of {{MaxServiceAccountsCount}}. New service accounts cannot be created
|
||||
Your organization has reached the Secrets Manager machine accounts limit of {{MaxServiceAccountsCount}}. New machine accounts cannot be created
|
||||
|
||||
For more information, please refer to the following help article: https://bitwarden.com/help/managing-users
|
||||
{{/BasicTextLayout}}
|
||||
|
12
src/Core/Models/Api/Request/PushDeviceRequestModel.cs
Normal file
12
src/Core/Models/Api/Request/PushDeviceRequestModel.cs
Normal 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; }
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Api;
|
||||
|
||||
@ -7,14 +8,14 @@ public class PushUpdateRequestModel
|
||||
public PushUpdateRequestModel()
|
||||
{ }
|
||||
|
||||
public PushUpdateRequestModel(IEnumerable<string> deviceIds, string organizationId)
|
||||
public PushUpdateRequestModel(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
||||
{
|
||||
DeviceIds = deviceIds;
|
||||
Devices = devices.Select(d => new PushDeviceRequestModel { Id = d.Key, Type = d.Value });
|
||||
OrganizationId = organizationId;
|
||||
}
|
||||
|
||||
[Required]
|
||||
public IEnumerable<string> DeviceIds { get; set; }
|
||||
public IEnumerable<PushDeviceRequestModel> Devices { get; set; }
|
||||
[Required]
|
||||
public string OrganizationId { get; set; }
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace Bit.Core.Models.StaticStore.Plans;
|
||||
|
||||
public record EnterprisePlan : Models.StaticStore.Plan
|
||||
public record EnterprisePlan : Plan
|
||||
{
|
||||
public EnterprisePlan(bool isAnnual)
|
||||
{
|
||||
@ -44,7 +44,7 @@ public record EnterprisePlan : Models.StaticStore.Plan
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 200;
|
||||
BaseServiceAccount = 50;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
@ -55,16 +55,16 @@ public record EnterprisePlan : Models.StaticStore.Plan
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-annually";
|
||||
SeatPrice = 144;
|
||||
AdditionalPricePerServiceAccount = 6;
|
||||
AdditionalPricePerServiceAccount = 12;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-monthly";
|
||||
SeatPrice = 13;
|
||||
AdditionalPricePerServiceAccount = 0.5M;
|
||||
AdditionalPricePerServiceAccount = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
102
src/Core/Models/StaticStore/Plans/EnterprisePlan2023.cs
Normal file
102
src/Core/Models/StaticStore/Plans/EnterprisePlan2023.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace Bit.Core.Models.StaticStore.Plans;
|
||||
|
||||
public record TeamsPlan : Models.StaticStore.Plan
|
||||
public record TeamsPlan : Plan
|
||||
{
|
||||
public TeamsPlan(bool isAnnual)
|
||||
{
|
||||
@ -37,7 +37,7 @@ public record TeamsPlan : Models.StaticStore.Plan
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 50;
|
||||
BaseServiceAccount = 20;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
@ -48,16 +48,16 @@ public record TeamsPlan : Models.StaticStore.Plan
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-annually";
|
||||
SeatPrice = 72;
|
||||
AdditionalPricePerServiceAccount = 6;
|
||||
AdditionalPricePerServiceAccount = 12;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-monthly";
|
||||
SeatPrice = 7;
|
||||
AdditionalPricePerServiceAccount = 0.5M;
|
||||
AdditionalPricePerServiceAccount = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
96
src/Core/Models/StaticStore/Plans/TeamsPlan2023.cs
Normal file
96
src/Core/Models/StaticStore/Plans/TeamsPlan2023.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -36,7 +36,7 @@ public record TeamsStarterPlan : Plan
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 50;
|
||||
BaseServiceAccount = 20;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
@ -45,9 +45,9 @@ public record TeamsStarterPlan : Plan
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-monthly";
|
||||
SeatPrice = 7;
|
||||
AdditionalPricePerServiceAccount = 0.5M;
|
||||
AdditionalPricePerServiceAccount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
72
src/Core/Models/StaticStore/Plans/TeamsStarterPlan2023.cs
Normal file
72
src/Core/Models/StaticStore/Plans/TeamsStarterPlan2023.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -118,7 +118,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, $"Error encountered notifying organization owners of service accounts limit reached.");
|
||||
_logger.LogError(e, $"Error encountered notifying organization owners of machine accounts limit reached.");
|
||||
}
|
||||
|
||||
}
|
||||
@ -253,12 +253,12 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
// Check if the organization has unlimited service accounts
|
||||
if (organization.SmServiceAccounts == null)
|
||||
{
|
||||
throw new BadRequestException("Organization has no service accounts limit, no need to adjust service accounts");
|
||||
throw new BadRequestException("Organization has no machine accounts limit, no need to adjust machine accounts");
|
||||
}
|
||||
|
||||
if (update.Autoscaling && update.SmServiceAccounts.Value < organization.SmServiceAccounts.Value)
|
||||
{
|
||||
throw new BadRequestException("Cannot use autoscaling to subtract service accounts.");
|
||||
throw new BadRequestException("Cannot use autoscaling to subtract machine accounts.");
|
||||
}
|
||||
|
||||
// Check plan maximum service accounts
|
||||
@ -267,7 +267,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
{
|
||||
var planMaxServiceAccounts = plan.SecretsManager.BaseServiceAccount +
|
||||
plan.SecretsManager.MaxAdditionalServiceAccount.GetValueOrDefault();
|
||||
throw new BadRequestException($"You have reached the maximum number of service accounts ({planMaxServiceAccounts}) for this plan.");
|
||||
throw new BadRequestException($"You have reached the maximum number of machine accounts ({planMaxServiceAccounts}) for this plan.");
|
||||
}
|
||||
|
||||
// Check autoscale maximum service accounts
|
||||
@ -275,21 +275,21 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
update.SmServiceAccounts.Value > update.MaxAutoscaleSmServiceAccounts.Value)
|
||||
{
|
||||
var message = update.Autoscaling
|
||||
? "Secrets Manager service account limit has been reached."
|
||||
: "Cannot set max service accounts autoscaling below service account amount.";
|
||||
? "Secrets Manager machine account limit has been reached."
|
||||
: "Cannot set max machine accounts autoscaling below machine account amount.";
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
|
||||
// Check minimum service accounts included with plan
|
||||
if (plan.SecretsManager.BaseServiceAccount > update.SmServiceAccounts.Value)
|
||||
{
|
||||
throw new BadRequestException($"Plan has a minimum of {plan.SecretsManager.BaseServiceAccount} service accounts.");
|
||||
throw new BadRequestException($"Plan has a minimum of {plan.SecretsManager.BaseServiceAccount} machine accounts.");
|
||||
}
|
||||
|
||||
// Check minimum service accounts required by business logic
|
||||
if (update.SmServiceAccounts.Value <= 0)
|
||||
{
|
||||
throw new BadRequestException("You must have at least 1 service account.");
|
||||
throw new BadRequestException("You must have at least 1 machine account.");
|
||||
}
|
||||
|
||||
// Check minimum service accounts currently in use by the organization
|
||||
@ -298,8 +298,8 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
var currentServiceAccounts = await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id);
|
||||
if (currentServiceAccounts > update.SmServiceAccounts)
|
||||
{
|
||||
throw new BadRequestException($"Your organization currently has {currentServiceAccounts} service accounts. " +
|
||||
$"You cannot decrease your subscription below your current service account usage.");
|
||||
throw new BadRequestException($"Your organization currently has {currentServiceAccounts} machine accounts. " +
|
||||
$"You cannot decrease your subscription below your current machine account usage.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -346,18 +346,18 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
if (update.SmServiceAccounts.HasValue && update.MaxAutoscaleSmServiceAccounts.Value < update.SmServiceAccounts.Value)
|
||||
{
|
||||
throw new BadRequestException(
|
||||
$"Cannot set max service accounts autoscaling below current service accounts count.");
|
||||
$"Cannot set max machine accounts autoscaling below current machine accounts count.");
|
||||
}
|
||||
|
||||
if (!plan.SecretsManager.AllowServiceAccountsAutoscale)
|
||||
{
|
||||
throw new BadRequestException("Your plan does not allow service accounts autoscaling.");
|
||||
throw new BadRequestException("Your plan does not allow machine accounts autoscaling.");
|
||||
}
|
||||
|
||||
if (plan.SecretsManager.MaxServiceAccounts.HasValue && update.MaxAutoscaleSmServiceAccounts.Value > plan.SecretsManager.MaxServiceAccounts)
|
||||
{
|
||||
throw new BadRequestException(string.Concat(
|
||||
$"Your plan has a service account limit of {plan.SecretsManager.MaxServiceAccounts}, ",
|
||||
$"Your plan has a machine account limit of {plan.SecretsManager.MaxServiceAccounts}, ",
|
||||
$"but you have specified a max autoscale count of {update.MaxAutoscaleSmServiceAccounts}.",
|
||||
"Reduce your max autoscale count."));
|
||||
}
|
||||
|
@ -330,9 +330,9 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
||||
if (currentServiceAccounts > newPlanServiceAccounts)
|
||||
{
|
||||
throw new BadRequestException(
|
||||
$"Your organization currently has {currentServiceAccounts} service accounts. " +
|
||||
$"Your new plan only allows {newSecretsManagerPlan.SecretsManager.MaxServiceAccounts} service accounts. " +
|
||||
"Remove some service accounts or increase your subscription.");
|
||||
$"Your organization currently has {currentServiceAccounts} machine accounts. " +
|
||||
$"Your new plan only allows {newSecretsManagerPlan.SecretsManager.MaxServiceAccounts} machine accounts. " +
|
||||
"Remove some machine accounts or increase your subscription.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ public interface IPushRegistrationService
|
||||
{
|
||||
Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
||||
string identifier, DeviceType type);
|
||||
Task DeleteRegistrationAsync(string deviceId);
|
||||
Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId);
|
||||
Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId);
|
||||
Task DeleteRegistrationAsync(string deviceId, DeviceType type);
|
||||
Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId);
|
||||
Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId);
|
||||
}
|
||||
|
@ -38,13 +38,13 @@ public class DeviceService : IDeviceService
|
||||
public async Task ClearTokenAsync(Device device)
|
||||
{
|
||||
await _deviceRepository.ClearPushTokenAsync(device.Id);
|
||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString(), device.Type);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Device device)
|
||||
{
|
||||
await _deviceRepository.DeleteAsync(device);
|
||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString(), device.Type);
|
||||
}
|
||||
|
||||
public async Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,
|
||||
|
@ -951,7 +951,7 @@ public class HandlebarsMailService : IMailService
|
||||
public async Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount,
|
||||
IEnumerable<string> ownerEmails)
|
||||
{
|
||||
var message = CreateDefaultMessage($"{organization.DisplayName()} Secrets Manager Service Accounts Limit Reached", ownerEmails);
|
||||
var message = CreateDefaultMessage($"{organization.DisplayName()} Secrets Manager Machine Accounts Limit Reached", ownerEmails);
|
||||
var model = new OrganizationServiceAccountsMaxReachedViewModel
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
|
@ -43,7 +43,8 @@ public class MultiServicePushNotificationService : IPushNotificationService
|
||||
}
|
||||
else
|
||||
{
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.NotificationHub.ConnectionString))
|
||||
var generalHub = globalSettings.NotificationHubs?.FirstOrDefault(h => h.HubType == NotificationHubType.General);
|
||||
if (CoreHelpers.SettingHasValue(generalHub?.ConnectionString))
|
||||
{
|
||||
_services.Add(new NotificationHubPushNotificationService(installationDeviceRepository,
|
||||
globalSettings, httpContextAccessor, hubLogger));
|
||||
|
@ -20,8 +20,9 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private NotificationHubClient _client = null;
|
||||
private ILogger _logger;
|
||||
private readonly List<NotificationHubClient> _clients = [];
|
||||
private readonly bool _enableTracing = false;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public NotificationHubPushNotificationService(
|
||||
IInstallationDeviceRepository installationDeviceRepository,
|
||||
@ -32,10 +33,18 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
||||
_installationDeviceRepository = installationDeviceRepository;
|
||||
_globalSettings = globalSettings;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_client = NotificationHubClient.CreateClientFromConnectionString(
|
||||
_globalSettings.NotificationHub.ConnectionString,
|
||||
_globalSettings.NotificationHub.HubName,
|
||||
_globalSettings.NotificationHub.EnableSendTracing);
|
||||
|
||||
foreach (var hub in globalSettings.NotificationHubs)
|
||||
{
|
||||
var client = NotificationHubClient.CreateClientFromConnectionString(
|
||||
hub.ConnectionString,
|
||||
hub.HubName,
|
||||
hub.EnableSendTracing);
|
||||
_clients.Add(client);
|
||||
|
||||
_enableTracing = _enableTracing || hub.EnableSendTracing;
|
||||
}
|
||||
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -255,16 +264,31 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
||||
|
||||
private async Task SendPayloadAsync(string tag, PushType type, object payload)
|
||||
{
|
||||
var outcome = await _client.SendTemplateNotificationAsync(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "type", ((byte)type).ToString() },
|
||||
{ "payload", JsonSerializer.Serialize(payload) }
|
||||
}, tag);
|
||||
if (_globalSettings.NotificationHub.EnableSendTracing)
|
||||
var tasks = new List<Task<NotificationOutcome>>();
|
||||
foreach (var client in _clients)
|
||||
{
|
||||
_logger.LogInformation("Azure Notification Hub Tracking ID: {id} | {type} push notification with {success} successes and {failure} failures with a payload of {@payload} and result of {@results}",
|
||||
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
|
||||
var task = client.SendTemplateNotificationAsync(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "type", ((byte)type).ToString() },
|
||||
{ "payload", JsonSerializer.Serialize(payload) }
|
||||
}, tag);
|
||||
tasks.Add(task);
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
if (_enableTracing)
|
||||
{
|
||||
for (var i = 0; i < tasks.Count; i++)
|
||||
{
|
||||
if (_clients[i].EnableTestSend)
|
||||
{
|
||||
var outcome = await tasks[i];
|
||||
_logger.LogInformation("Azure Notification Hub Tracking ID: {id} | {type} push notification with {success} successes and {failure} failures with a payload of {@payload} and result of {@results}",
|
||||
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Azure.NotificationHubs;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
@ -10,18 +11,36 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
||||
{
|
||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
private NotificationHubClient _client = null;
|
||||
private readonly ILogger<NotificationHubPushRegistrationService> _logger;
|
||||
private Dictionary<NotificationHubType, NotificationHubClient> _clients = [];
|
||||
|
||||
public NotificationHubPushRegistrationService(
|
||||
IInstallationDeviceRepository installationDeviceRepository,
|
||||
GlobalSettings globalSettings)
|
||||
GlobalSettings globalSettings,
|
||||
ILogger<NotificationHubPushRegistrationService> logger)
|
||||
{
|
||||
_installationDeviceRepository = installationDeviceRepository;
|
||||
_globalSettings = globalSettings;
|
||||
_client = NotificationHubClient.CreateClientFromConnectionString(
|
||||
_globalSettings.NotificationHub.ConnectionString,
|
||||
_globalSettings.NotificationHub.HubName);
|
||||
_logger = logger;
|
||||
|
||||
// Is this dirty to do in the ctor?
|
||||
void addHub(NotificationHubType type)
|
||||
{
|
||||
var hubRegistration = globalSettings.NotificationHubs.FirstOrDefault(
|
||||
h => h.HubType == type && h.EnableRegistration);
|
||||
if (hubRegistration != null)
|
||||
{
|
||||
var client = NotificationHubClient.CreateClientFromConnectionString(
|
||||
hubRegistration.ConnectionString,
|
||||
hubRegistration.HubName,
|
||||
hubRegistration.EnableSendTracing);
|
||||
_clients.Add(type, client);
|
||||
}
|
||||
}
|
||||
|
||||
addHub(NotificationHubType.General);
|
||||
addHub(NotificationHubType.iOS);
|
||||
addHub(NotificationHubType.Android);
|
||||
}
|
||||
|
||||
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
||||
@ -84,7 +103,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
||||
BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate,
|
||||
userId, identifier);
|
||||
|
||||
await _client.CreateOrUpdateInstallationAsync(installation);
|
||||
await GetClient(type).CreateOrUpdateInstallationAsync(installation);
|
||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
||||
{
|
||||
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
||||
@ -119,11 +138,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
||||
installation.Templates.Add(fullTemplateId, template);
|
||||
}
|
||||
|
||||
public async Task DeleteRegistrationAsync(string deviceId)
|
||||
public async Task DeleteRegistrationAsync(string deviceId, DeviceType deviceType)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _client.DeleteInstallationAsync(deviceId);
|
||||
await GetClient(deviceType).DeleteInstallationAsync(deviceId);
|
||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
||||
{
|
||||
await _installationDeviceRepository.DeleteAsync(new InstallationDeviceEntity(deviceId));
|
||||
@ -135,31 +154,31 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
||||
public async Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
||||
{
|
||||
await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Add, $"organizationId:{organizationId}");
|
||||
if (deviceIds.Any() && InstallationDeviceEntity.IsInstallationDeviceId(deviceIds.First()))
|
||||
await PatchTagsForUserDevicesAsync(devices, UpdateOperationType.Add, $"organizationId:{organizationId}");
|
||||
if (devices.Any() && InstallationDeviceEntity.IsInstallationDeviceId(devices.First().Key))
|
||||
{
|
||||
var entities = deviceIds.Select(e => new InstallationDeviceEntity(e));
|
||||
var entities = devices.Select(e => new InstallationDeviceEntity(e.Key));
|
||||
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
||||
public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
||||
{
|
||||
await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Remove,
|
||||
await PatchTagsForUserDevicesAsync(devices, UpdateOperationType.Remove,
|
||||
$"organizationId:{organizationId}");
|
||||
if (deviceIds.Any() && InstallationDeviceEntity.IsInstallationDeviceId(deviceIds.First()))
|
||||
if (devices.Any() && InstallationDeviceEntity.IsInstallationDeviceId(devices.First().Key))
|
||||
{
|
||||
var entities = deviceIds.Select(e => new InstallationDeviceEntity(e));
|
||||
var entities = devices.Select(e => new InstallationDeviceEntity(e.Key));
|
||||
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PatchTagsForUserDevicesAsync(IEnumerable<string> deviceIds, UpdateOperationType op,
|
||||
private async Task PatchTagsForUserDevicesAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, UpdateOperationType op,
|
||||
string tag)
|
||||
{
|
||||
if (!deviceIds.Any())
|
||||
if (!devices.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -179,11 +198,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
||||
operation.Path += $"/{tag}";
|
||||
}
|
||||
|
||||
foreach (var id in deviceIds)
|
||||
foreach (var device in devices)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _client.PatchInstallationAsync(id, new List<PartialUpdateOperation> { operation });
|
||||
await GetClient(device.Value).PatchInstallationAsync(device.Key, new List<PartialUpdateOperation> { operation });
|
||||
}
|
||||
catch (Exception e) when (e.InnerException == null || !e.InnerException.Message.Contains("(404) Not Found"))
|
||||
{
|
||||
@ -191,4 +210,54 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private NotificationHubClient GetClient(DeviceType deviceType)
|
||||
{
|
||||
var hubType = NotificationHubType.General;
|
||||
switch (deviceType)
|
||||
{
|
||||
case DeviceType.Android:
|
||||
hubType = NotificationHubType.Android;
|
||||
break;
|
||||
case DeviceType.iOS:
|
||||
hubType = NotificationHubType.iOS;
|
||||
break;
|
||||
case DeviceType.ChromeExtension:
|
||||
case DeviceType.FirefoxExtension:
|
||||
case DeviceType.OperaExtension:
|
||||
case DeviceType.EdgeExtension:
|
||||
case DeviceType.VivaldiExtension:
|
||||
case DeviceType.SafariExtension:
|
||||
hubType = NotificationHubType.GeneralBrowserExtension;
|
||||
break;
|
||||
case DeviceType.WindowsDesktop:
|
||||
case DeviceType.MacOsDesktop:
|
||||
case DeviceType.LinuxDesktop:
|
||||
hubType = NotificationHubType.GeneralDesktop;
|
||||
break;
|
||||
case DeviceType.ChromeBrowser:
|
||||
case DeviceType.FirefoxBrowser:
|
||||
case DeviceType.OperaBrowser:
|
||||
case DeviceType.EdgeBrowser:
|
||||
case DeviceType.IEBrowser:
|
||||
case DeviceType.UnknownBrowser:
|
||||
case DeviceType.SafariBrowser:
|
||||
case DeviceType.VivaldiBrowser:
|
||||
hubType = NotificationHubType.GeneralWeb;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!_clients.ContainsKey(hubType))
|
||||
{
|
||||
_logger.LogWarning("No hub client for '{0}'. Using general hub instead.", hubType);
|
||||
hubType = NotificationHubType.General;
|
||||
if (!_clients.ContainsKey(hubType))
|
||||
{
|
||||
throw new Exception("No general hub client found.");
|
||||
}
|
||||
}
|
||||
return _clients[hubType];
|
||||
}
|
||||
}
|
||||
|
@ -38,30 +38,37 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi
|
||||
await SendAsync(HttpMethod.Post, "push/register", requestModel);
|
||||
}
|
||||
|
||||
public async Task DeleteRegistrationAsync(string deviceId)
|
||||
public async Task DeleteRegistrationAsync(string deviceId, DeviceType type)
|
||||
{
|
||||
await SendAsync(HttpMethod.Delete, string.Concat("push/", deviceId));
|
||||
var requestModel = new PushDeviceRequestModel
|
||||
{
|
||||
Id = deviceId,
|
||||
Type = type,
|
||||
};
|
||||
await SendAsync(HttpMethod.Post, "push/delete", requestModel);
|
||||
}
|
||||
|
||||
public async Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
||||
public async Task AddUserRegistrationOrganizationAsync(
|
||||
IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
||||
{
|
||||
if (!deviceIds.Any())
|
||||
if (!devices.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
|
||||
var requestModel = new PushUpdateRequestModel(devices, organizationId);
|
||||
await SendAsync(HttpMethod.Put, "push/add-organization", requestModel);
|
||||
}
|
||||
|
||||
public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
||||
public async Task DeleteUserRegistrationOrganizationAsync(
|
||||
IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
||||
{
|
||||
if (!deviceIds.Any())
|
||||
if (!devices.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
|
||||
var requestModel = new PushUpdateRequestModel(devices, organizationId);
|
||||
await SendAsync(HttpMethod.Put, "push/delete-organization", requestModel);
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ namespace Bit.Core.Services;
|
||||
|
||||
public class NoopPushRegistrationService : IPushRegistrationService
|
||||
{
|
||||
public Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
||||
public Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
@ -15,12 +15,12 @@ public class NoopPushRegistrationService : IPushRegistrationService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task DeleteRegistrationAsync(string deviceId)
|
||||
public Task DeleteRegistrationAsync(string deviceId, DeviceType deviceType)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
||||
public Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Auth.Settings;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
namespace Bit.Core.Settings;
|
||||
@ -64,7 +65,7 @@ public class GlobalSettings : IGlobalSettings
|
||||
public virtual SentrySettings Sentry { get; set; } = new SentrySettings();
|
||||
public virtual SyslogSettings Syslog { get; set; } = new SyslogSettings();
|
||||
public virtual ILogLevelSettings MinLogLevel { get; set; } = new LogLevelSettings();
|
||||
public virtual NotificationHubSettings NotificationHub { get; set; } = new NotificationHubSettings();
|
||||
public virtual List<NotificationHubSettings> NotificationHubs { get; set; } = new();
|
||||
public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings();
|
||||
public virtual DuoSettings Duo { get; set; } = new DuoSettings();
|
||||
public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings();
|
||||
@ -414,12 +415,16 @@ public class GlobalSettings : IGlobalSettings
|
||||
set => _connectionString = value.Trim('"');
|
||||
}
|
||||
public string HubName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables TestSend on the Azure Notification Hub, which allows tracing of the request through the hub and to the platform-specific push notification service (PNS).
|
||||
/// Enabling this will result in delayed responses because the Hub must wait on delivery to the PNS. This should ONLY be enabled in a non-production environment, as results are throttled.
|
||||
/// </summary>
|
||||
public bool EnableSendTracing { get; set; } = false;
|
||||
/// <summary>
|
||||
/// At least one hub configuration should have registration enabled, preferably the General hub as a safety net.
|
||||
/// </summary>
|
||||
public bool EnableRegistration { get; set; }
|
||||
public NotificationHubType HubType { get; set; }
|
||||
}
|
||||
|
||||
public class YubicoSettings
|
||||
|
@ -114,8 +114,13 @@ public static class StaticStore
|
||||
new TeamsPlan(true),
|
||||
new TeamsPlan(false),
|
||||
|
||||
new Enterprise2023Plan(true),
|
||||
new Enterprise2023Plan(false),
|
||||
new Enterprise2020Plan(true),
|
||||
new Enterprise2020Plan(false),
|
||||
new TeamsStarterPlan2023(),
|
||||
new Teams2023Plan(true),
|
||||
new Teams2023Plan(false),
|
||||
new Teams2020Plan(true),
|
||||
new Teams2020Plan(false),
|
||||
new FamiliesPlan(),
|
||||
|
@ -26,7 +26,20 @@ public class OrganizationMapperProfile : Profile
|
||||
{
|
||||
public OrganizationMapperProfile()
|
||||
{
|
||||
CreateMap<Core.AdminConsole.Entities.Organization, Organization>().ReverseMap();
|
||||
CreateMap<Core.AdminConsole.Entities.Organization, Organization>()
|
||||
.ForMember(org => org.Ciphers, opt => opt.Ignore())
|
||||
.ForMember(org => org.OrganizationUsers, opt => opt.Ignore())
|
||||
.ForMember(org => org.Groups, opt => opt.Ignore())
|
||||
.ForMember(org => org.Policies, opt => opt.Ignore())
|
||||
.ForMember(org => org.Collections, opt => opt.Ignore())
|
||||
.ForMember(org => org.SsoConfigs, opt => opt.Ignore())
|
||||
.ForMember(org => org.SsoUsers, opt => opt.Ignore())
|
||||
.ForMember(org => org.Transactions, opt => opt.Ignore())
|
||||
.ForMember(org => org.ApiKeys, opt => opt.Ignore())
|
||||
.ForMember(org => org.Connections, opt => opt.Ignore())
|
||||
.ForMember(org => org.Domains, opt => opt.Ignore())
|
||||
.ReverseMap();
|
||||
|
||||
CreateProjection<Organization, SelfHostedOrganizationDetails>()
|
||||
.ForMember(sd => sd.CollectionCount, opt => opt.MapFrom(o => o.Collections.Count))
|
||||
.ForMember(sd => sd.GroupCount, opt => opt.MapFrom(o => o.Groups.Count))
|
||||
|
@ -50,9 +50,10 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var organizations = await GetDbSet(dbContext)
|
||||
.Select(e => e.OrganizationUsers
|
||||
.Where(ou => ou.UserId == userId)
|
||||
.Select(ou => ou.Organization))
|
||||
.SelectMany(e => e.OrganizationUsers
|
||||
.Where(ou => ou.UserId == userId))
|
||||
.Include(ou => ou.Organization)
|
||||
.Select(ou => ou.Organization)
|
||||
.ToListAsync();
|
||||
return Mapper.Map<List<Core.AdminConsole.Entities.Organization>>(organizations);
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Xunit;
|
||||
|
||||
@ -25,9 +24,6 @@ public class CreateOrganizationDomainCommandTests
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetDomainByOrgIdAndDomainNameAsync(orgDomain.OrganizationId, orgDomain.DomainName)
|
||||
.ReturnsNull();
|
||||
sutProvider.GetDependency<IDnsResolverService>()
|
||||
.ResolveAsync(orgDomain.DomainName, orgDomain.Txt)
|
||||
.Returns(false);
|
||||
orgDomain.SetNextRunDate(12);
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.CreateAsync(orgDomain)
|
||||
@ -38,12 +34,12 @@ public class CreateOrganizationDomainCommandTests
|
||||
|
||||
Assert.Equal(orgDomain.Id, result.Id);
|
||||
Assert.Equal(orgDomain.OrganizationId, result.OrganizationId);
|
||||
Assert.NotNull(result.LastCheckedDate);
|
||||
Assert.Null(result.LastCheckedDate);
|
||||
Assert.Equal(orgDomain.Txt, result.Txt);
|
||||
Assert.Equal(orgDomain.Txt.Length == 47, result.Txt.Length == 47);
|
||||
Assert.Equal(orgDomain.NextRunDate, result.NextRunDate);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||
.LogOrganizationDomainEventAsync(Arg.Any<OrganizationDomain>(), EventType.OrganizationDomain_Added);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||
.LogOrganizationDomainEventAsync(Arg.Any<OrganizationDomain>(), Arg.Is<EventType>(x => x == EventType.OrganizationDomain_NotVerified));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -79,52 +75,4 @@ public class CreateOrganizationDomainCommandTests
|
||||
var exception = await Assert.ThrowsAsync<ConflictException>(requestAction);
|
||||
Assert.Contains("A domain already exists for this organization.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_ShouldNotSetVerifiedDate_WhenDomainCannotBeResolved(OrganizationDomain orgDomain,
|
||||
SutProvider<CreateOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetClaimedDomainsByDomainNameAsync(orgDomain.DomainName)
|
||||
.Returns(new List<OrganizationDomain>());
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetDomainByOrgIdAndDomainNameAsync(orgDomain.OrganizationId, orgDomain.DomainName)
|
||||
.ReturnsNull();
|
||||
sutProvider.GetDependency<IDnsResolverService>()
|
||||
.ResolveAsync(orgDomain.DomainName, orgDomain.Txt)
|
||||
.Throws(new DnsQueryException(""));
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.CreateAsync(orgDomain)
|
||||
.Returns(orgDomain);
|
||||
|
||||
await sutProvider.Sut.CreateAsync(orgDomain);
|
||||
|
||||
Assert.Null(orgDomain.VerifiedDate);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_ShouldSetVerifiedDateAndLogEvent_WhenDomainIsResolved(OrganizationDomain orgDomain,
|
||||
SutProvider<CreateOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetClaimedDomainsByDomainNameAsync(orgDomain.DomainName)
|
||||
.Returns(new List<OrganizationDomain>());
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetDomainByOrgIdAndDomainNameAsync(orgDomain.OrganizationId, orgDomain.DomainName)
|
||||
.ReturnsNull();
|
||||
sutProvider.GetDependency<IDnsResolverService>()
|
||||
.ResolveAsync(orgDomain.DomainName, orgDomain.Txt)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.CreateAsync(orgDomain)
|
||||
.Returns(orgDomain);
|
||||
|
||||
var result = await sutProvider.Sut.CreateAsync(orgDomain);
|
||||
|
||||
Assert.NotNull(result.VerifiedDate);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||
.LogOrganizationDomainEventAsync(Arg.Any<OrganizationDomain>(), EventType.OrganizationDomain_Added);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||
.LogOrganizationDomainEventAsync(Arg.Any<OrganizationDomain>(), Arg.Is<EventType>(x => x == EventType.OrganizationDomain_Verified));
|
||||
}
|
||||
}
|
||||
|
@ -410,7 +410,7 @@ public class OrganizationServiceTests
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SignUpAsync(signup));
|
||||
Assert.Contains("Plan does not allow additional Service Accounts.", exception.Message);
|
||||
Assert.Contains("Plan does not allow additional Machine Accounts.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -444,7 +444,7 @@ public class OrganizationServiceTests
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SignUpAsync(signup));
|
||||
Assert.Contains("You can't subtract Service Accounts!", exception.Message);
|
||||
Assert.Contains("You can't subtract Machine Accounts!", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -2208,7 +2208,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
AdditionalSeats = 3
|
||||
};
|
||||
var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup));
|
||||
Assert.Contains("Plan does not allow additional Service Accounts.", exception.Message);
|
||||
Assert.Contains("Plan does not allow additional Machine Accounts.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -2249,7 +2249,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
AdditionalSeats = 5
|
||||
};
|
||||
var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup));
|
||||
Assert.Contains("You can't subtract Service Accounts!", exception.Message);
|
||||
Assert.Contains("You can't subtract Machine Accounts!", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
@ -447,7 +447,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustServiceAccounts(1);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("Organization has no service accounts limit, no need to adjust service accounts", exception.Message);
|
||||
Assert.Contains("Organization has no machine accounts limit, no need to adjust machine accounts", exception.Message);
|
||||
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||
}
|
||||
|
||||
@ -460,7 +460,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustServiceAccounts(-2);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("Cannot use autoscaling to subtract service accounts.", exception.Message);
|
||||
Assert.Contains("Cannot use autoscaling to subtract machine accounts.", exception.Message);
|
||||
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||
}
|
||||
|
||||
@ -475,7 +475,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustServiceAccounts(1);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("You have reached the maximum number of service accounts (3) for this plan",
|
||||
Assert.Contains("You have reached the maximum number of machine accounts (3) for this plan",
|
||||
exception.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||
}
|
||||
@ -492,7 +492,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustServiceAccounts(2);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("Secrets Manager service account limit has been reached.", exception.Message);
|
||||
Assert.Contains("Secrets Manager machine account limit has been reached.", exception.Message);
|
||||
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||
}
|
||||
|
||||
@ -516,7 +516,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("Cannot set max service accounts autoscaling below service account amount", exception.Message);
|
||||
Assert.Contains("Cannot set max machine accounts autoscaling below machine account amount", exception.Message);
|
||||
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||
}
|
||||
|
||||
@ -526,7 +526,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
Organization organization,
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
const int newSmServiceAccounts = 199;
|
||||
const int newSmServiceAccounts = 49;
|
||||
|
||||
organization.SmServiceAccounts = newSmServiceAccounts - 10;
|
||||
|
||||
@ -537,7 +537,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("Plan has a minimum of 200 service accounts", exception.Message);
|
||||
Assert.Contains("Plan has a minimum of 50 machine accounts", exception.Message);
|
||||
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||
}
|
||||
|
||||
@ -570,7 +570,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
.Returns(currentServiceAccounts);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("Your organization currently has 301 service accounts. You cannot decrease your subscription below your current service account usage", exception.Message);
|
||||
Assert.Contains("Your organization currently has 301 machine accounts. You cannot decrease your subscription below your current machine account usage", exception.Message);
|
||||
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||
}
|
||||
|
||||
@ -648,7 +648,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false) { MaxAutoscaleSmServiceAccounts = 3 };
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("Your plan does not allow service accounts autoscaling.", exception.Message);
|
||||
Assert.Contains("Your plan does not allow machine accounts autoscaling.", exception.Message);
|
||||
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||
}
|
||||
|
||||
|
@ -192,7 +192,7 @@ public class UpgradeOrganizationPlanCommandTests
|
||||
.GetServiceAccountCountByOrganizationIdAsync(organization.Id).Returns(currentServiceAccounts);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
|
||||
Assert.Contains($"Your organization currently has {currentServiceAccounts} service accounts. Your new plan only allows", exception.Message);
|
||||
Assert.Contains($"Your organization currently has {currentServiceAccounts} machine accounts. Your new plan only allows", exception.Message);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs().ReplaceAndUpdateCacheAsync(default);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
@ -11,16 +12,19 @@ public class NotificationHubPushRegistrationServiceTests
|
||||
private readonly NotificationHubPushRegistrationService _sut;
|
||||
|
||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||
private readonly ILogger<NotificationHubPushRegistrationService> _logger;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public NotificationHubPushRegistrationServiceTests()
|
||||
{
|
||||
_installationDeviceRepository = Substitute.For<IInstallationDeviceRepository>();
|
||||
_logger = Substitute.For<ILogger<NotificationHubPushRegistrationService>>();
|
||||
_globalSettings = new GlobalSettings();
|
||||
|
||||
_sut = new NotificationHubPushRegistrationService(
|
||||
_installationDeviceRepository,
|
||||
_globalSettings
|
||||
_globalSettings,
|
||||
_logger
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ public class StaticStoreTests
|
||||
var plans = StaticStore.Plans.ToList();
|
||||
Assert.NotNull(plans);
|
||||
Assert.NotEmpty(plans);
|
||||
Assert.Equal(17, plans.Count);
|
||||
Assert.Equal(22, plans.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
using Bit.Core.Entities;
|
||||
using AutoMapper;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Test.AutoFixture.Attributes;
|
||||
using Bit.Infrastructure.EFIntegration.Test.AutoFixture;
|
||||
using Bit.Infrastructure.EFIntegration.Test.Repositories.EqualityComparers;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||
using Xunit;
|
||||
using EfRepo = Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Organization = Bit.Core.AdminConsole.Entities.Organization;
|
||||
@ -13,6 +15,13 @@ namespace Bit.Infrastructure.EFIntegration.Test.Repositories;
|
||||
|
||||
public class OrganizationRepositoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void ValidateOrganizationMappings_ReturnsSuccess()
|
||||
{
|
||||
var config = new MapperConfiguration(cfg => cfg.AddProfile<OrganizationMapperProfile>());
|
||||
config.AssertConfigurationIsValid();
|
||||
}
|
||||
|
||||
[CiSkippedTheory, EfOrganizationAutoData]
|
||||
public async Task CreateAsync_Works_DataMatches(
|
||||
Organization organization,
|
||||
|
Reference in New Issue
Block a user