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