mirror of
https://github.com/bitwarden/server.git
synced 2025-04-21 04:55:08 -05:00
Merge branch 'main' into add-docker-arm64-builds
This commit is contained in:
commit
632f9bcdbe
7
.github/workflows/version-bump.yml
vendored
7
.github/workflows/version-bump.yml
vendored
@ -198,6 +198,13 @@ jobs:
|
|||||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||||
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
||||||
|
|
||||||
|
- name: Report upcoming release version to Slack
|
||||||
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
|
uses: bitwarden/gh-actions/report-upcoming-release-version@main
|
||||||
|
with:
|
||||||
|
version: ${{ steps.set-final-version-output.outputs.version }}
|
||||||
|
project: ${{ github.repository }}
|
||||||
|
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
|
||||||
cut_rc:
|
cut_rc:
|
||||||
name: Cut RC branch
|
name: Cut RC branch
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
<Version>2024.5.0</Version>
|
<Version>2024.5.1</Version>
|
||||||
|
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
@ -46,6 +46,13 @@ public class CreateProviderCommand : ICreateProviderCommand
|
|||||||
throw new BadRequestException("Invalid owner. Owner must be an existing Bitwarden user.");
|
throw new BadRequestException("Invalid owner. Owner must be an existing Bitwarden user.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||||
|
|
||||||
|
if (isConsolidatedBillingEnabled)
|
||||||
|
{
|
||||||
|
provider.Gateway = GatewayType.Stripe;
|
||||||
|
}
|
||||||
|
|
||||||
await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Pending);
|
await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Pending);
|
||||||
|
|
||||||
var providerUser = new ProviderUser
|
var providerUser = new ProviderUser
|
||||||
@ -56,8 +63,6 @@ public class CreateProviderCommand : ICreateProviderCommand
|
|||||||
Status = ProviderUserStatusType.Confirmed,
|
Status = ProviderUserStatusType.Confirmed,
|
||||||
};
|
};
|
||||||
|
|
||||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
|
||||||
|
|
||||||
if (isConsolidatedBillingEnabled)
|
if (isConsolidatedBillingEnabled)
|
||||||
{
|
{
|
||||||
var providerPlans = new List<ProviderPlan>
|
var providerPlans = new List<ProviderPlan>
|
||||||
|
@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Providers.Interfaces;
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Commands;
|
using Bit.Core.Billing.Commands;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -76,6 +77,35 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
|
|
||||||
organization.BillingEmail = organizationOwnerEmails.MinBy(email => email);
|
organization.BillingEmail = organizationOwnerEmails.MinBy(email => email);
|
||||||
|
|
||||||
|
await ResetOrganizationBillingAsync(organization, provider, organizationOwnerEmails);
|
||||||
|
|
||||||
|
await _organizationRepository.ReplaceAsync(organization);
|
||||||
|
|
||||||
|
await _providerOrganizationRepository.DeleteAsync(providerOrganization);
|
||||||
|
|
||||||
|
await _eventService.LogProviderOrganizationEventAsync(
|
||||||
|
providerOrganization,
|
||||||
|
EventType.ProviderOrganization_Removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When a client organization is unlinked from a provider, we have to check if they're Stripe-enabled
|
||||||
|
/// and, if they are, we remove their MSP discount and set their Subscription to `send_invoice`. This is because
|
||||||
|
/// the provider's payment method will be removed from their Stripe customer causing ensuing charges to fail. Lastly,
|
||||||
|
/// we email the organization owners letting them know they need to add a new payment method.
|
||||||
|
/// </summary>
|
||||||
|
private async Task ResetOrganizationBillingAsync(
|
||||||
|
Organization organization,
|
||||||
|
Provider provider,
|
||||||
|
IEnumerable<string> organizationOwnerEmails)
|
||||||
|
{
|
||||||
|
if (!organization.IsStripeEnabled())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||||
|
|
||||||
var customerUpdateOptions = new CustomerUpdateOptions
|
var customerUpdateOptions = new CustomerUpdateOptions
|
||||||
{
|
{
|
||||||
Coupon = string.Empty,
|
Coupon = string.Empty,
|
||||||
@ -84,11 +114,10 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
|
|
||||||
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, customerUpdateOptions);
|
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, customerUpdateOptions);
|
||||||
|
|
||||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
|
||||||
|
|
||||||
if (isConsolidatedBillingEnabled && provider.Status == ProviderStatusType.Billable)
|
if (isConsolidatedBillingEnabled && provider.Status == ProviderStatusType.Billable)
|
||||||
{
|
{
|
||||||
var plan = StaticStore.GetPlan(organization.PlanType).PasswordManager;
|
var plan = StaticStore.GetPlan(organization.PlanType).PasswordManager;
|
||||||
|
|
||||||
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
||||||
{
|
{
|
||||||
Customer = organization.GatewayCustomerId,
|
Customer = organization.GatewayCustomerId,
|
||||||
@ -103,8 +132,11 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
|
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
|
||||||
Items = [new SubscriptionItemOptions { Price = plan.StripeSeatPlanId, Quantity = organization.Seats }]
|
Items = [new SubscriptionItemOptions { Price = plan.StripeSeatPlanId, Quantity = organization.Seats }]
|
||||||
};
|
};
|
||||||
|
|
||||||
var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||||
|
|
||||||
organization.GatewaySubscriptionId = subscription.Id;
|
organization.GatewaySubscriptionId = subscription.Id;
|
||||||
|
|
||||||
await _scaleSeatsCommand.ScalePasswordManagerSeats(provider, organization.PlanType,
|
await _scaleSeatsCommand.ScalePasswordManagerSeats(provider, organization.PlanType,
|
||||||
-(organization.Seats ?? 0));
|
-(organization.Seats ?? 0));
|
||||||
}
|
}
|
||||||
@ -115,21 +147,14 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
CollectionMethod = "send_invoice",
|
CollectionMethod = "send_invoice",
|
||||||
DaysUntilDue = 30
|
DaysUntilDue = 30
|
||||||
};
|
};
|
||||||
|
|
||||||
await _stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, subscriptionUpdateOptions);
|
await _stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, subscriptionUpdateOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _organizationRepository.ReplaceAsync(organization);
|
|
||||||
|
|
||||||
await _mailService.SendProviderUpdatePaymentMethod(
|
await _mailService.SendProviderUpdatePaymentMethod(
|
||||||
organization.Id,
|
organization.Id,
|
||||||
organization.Name,
|
organization.Name,
|
||||||
provider.Name,
|
provider.Name,
|
||||||
organizationOwnerEmails);
|
organizationOwnerEmails);
|
||||||
|
|
||||||
await _providerOrganizationRepository.DeleteAsync(providerOrganization);
|
|
||||||
|
|
||||||
await _eventService.LogProviderOrganizationEventAsync(
|
|
||||||
providerOrganization,
|
|
||||||
EventType.ProviderOrganization_Removed);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,17 +14,23 @@ namespace Bit.Scim.Users;
|
|||||||
|
|
||||||
public class PostUserCommand : IPostUserCommand
|
public class PostUserCommand : IPostUserCommand
|
||||||
{
|
{
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
|
private readonly IPaymentService _paymentService;
|
||||||
private readonly IScimContext _scimContext;
|
private readonly IScimContext _scimContext;
|
||||||
|
|
||||||
public PostUserCommand(
|
public PostUserCommand(
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IOrganizationService organizationService,
|
IOrganizationService organizationService,
|
||||||
|
IPaymentService paymentService,
|
||||||
IScimContext scimContext)
|
IScimContext scimContext)
|
||||||
{
|
{
|
||||||
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
|
_paymentService = paymentService;
|
||||||
_scimContext = scimContext;
|
_scimContext = scimContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,8 +86,13 @@ public class PostUserCommand : IPostUserCommand
|
|||||||
throw new ConflictException();
|
throw new ConflictException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
|
||||||
|
var hasStandaloneSecretsManager = await _paymentService.HasSecretsManagerStandalone(organization);
|
||||||
|
|
||||||
var invitedOrgUser = await _organizationService.InviteUserAsync(organizationId, EventSystemUser.SCIM, email,
|
var invitedOrgUser = await _organizationService.InviteUserAsync(organizationId, EventSystemUser.SCIM, email,
|
||||||
OrganizationUserType.User, false, externalId, new List<CollectionAccessSelection>(), new List<Guid>());
|
OrganizationUserType.User, false, externalId, new List<CollectionAccessSelection>(), new List<Guid>(), hasStandaloneSecretsManager);
|
||||||
|
|
||||||
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(invitedOrgUser.Id);
|
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(invitedOrgUser.Id);
|
||||||
|
|
||||||
return orgUser;
|
return orgUser;
|
||||||
|
@ -14,6 +14,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
|
|||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using IMailService = Bit.Core.Services.IMailService;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Test.AdminConsole.ProviderFeatures;
|
namespace Bit.Commercial.Core.Test.AdminConsole.ProviderFeatures;
|
||||||
|
|
||||||
@ -83,6 +84,55 @@ public class RemoveOrganizationFromProviderCommandTests
|
|||||||
Assert.Equal("Organization must have at least one confirmed owner.", exception.Message);
|
Assert.Equal("Organization must have at least one confirmed owner.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RemoveOrganizationFromProvider_NoStripeObjects_MakesCorrectInvocations(
|
||||||
|
Provider provider,
|
||||||
|
ProviderOrganization providerOrganization,
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
|
||||||
|
{
|
||||||
|
organization.GatewayCustomerId = null;
|
||||||
|
organization.GatewaySubscriptionId = null;
|
||||||
|
|
||||||
|
providerOrganization.ProviderId = provider.Id;
|
||||||
|
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
|
||||||
|
providerOrganization.OrganizationId,
|
||||||
|
Array.Empty<Guid>(),
|
||||||
|
includeProvider: false)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
var organizationOwnerEmails = new List<string> { "a@gmail.com", "b@gmail.com" };
|
||||||
|
|
||||||
|
organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns(organizationOwnerEmails);
|
||||||
|
|
||||||
|
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||||
|
|
||||||
|
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(
|
||||||
|
org => org.Id == organization.Id && org.BillingEmail == "a@gmail.com"));
|
||||||
|
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
|
||||||
|
await stripeAdapter.DidNotReceiveWithAnyArgs().CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
|
||||||
|
|
||||||
|
await stripeAdapter.DidNotReceiveWithAnyArgs().SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IMailService>().DidNotReceiveWithAnyArgs().SendProviderUpdatePaymentMethod(
|
||||||
|
Arg.Any<Guid>(),
|
||||||
|
Arg.Any<string>(),
|
||||||
|
Arg.Any<string>(),
|
||||||
|
Arg.Any<IEnumerable<string>>());
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
|
||||||
|
.DeleteAsync(providerOrganization);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(1).LogProviderOrganizationEventAsync(
|
||||||
|
providerOrganization,
|
||||||
|
EventType.ProviderOrganization_Removed);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task RemoveOrganizationFromProvider_MakesCorrectInvocations__FeatureFlagOff(
|
public async Task RemoveOrganizationFromProvider_MakesCorrectInvocations__FeatureFlagOff(
|
||||||
Provider provider,
|
Provider provider,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
@ -19,7 +20,7 @@ public class PostUserCommandTests
|
|||||||
{
|
{
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task PostUser_Success(SutProvider<PostUserCommand> sutProvider, string externalId, Guid organizationId, List<BaseScimUserModel.EmailModel> emails, ICollection<OrganizationUserUserDetails> organizationUsers, Core.Entities.OrganizationUser newUser)
|
public async Task PostUser_Success(SutProvider<PostUserCommand> sutProvider, string externalId, Guid organizationId, List<BaseScimUserModel.EmailModel> emails, ICollection<OrganizationUserUserDetails> organizationUsers, Core.Entities.OrganizationUser newUser, Organization organization)
|
||||||
{
|
{
|
||||||
var scimUserRequestModel = new ScimUserRequestModel
|
var scimUserRequestModel = new ScimUserRequestModel
|
||||||
{
|
{
|
||||||
@ -33,16 +34,20 @@ public class PostUserCommandTests
|
|||||||
.GetManyDetailsByOrganizationAsync(organizationId)
|
.GetManyDetailsByOrganizationAsync(organizationId)
|
||||||
.Returns(organizationUsers);
|
.Returns(organizationUsers);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPaymentService>().HasSecretsManagerStandalone(organization).Returns(true);
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationService>()
|
sutProvider.GetDependency<IOrganizationService>()
|
||||||
.InviteUserAsync(organizationId, EventSystemUser.SCIM, scimUserRequestModel.PrimaryEmail.ToLowerInvariant(),
|
.InviteUserAsync(organizationId, EventSystemUser.SCIM, scimUserRequestModel.PrimaryEmail.ToLowerInvariant(),
|
||||||
OrganizationUserType.User, false, externalId, Arg.Any<List<CollectionAccessSelection>>(),
|
OrganizationUserType.User, false, externalId, Arg.Any<List<CollectionAccessSelection>>(),
|
||||||
Arg.Any<List<Guid>>())
|
Arg.Any<List<Guid>>(), true)
|
||||||
.Returns(newUser);
|
.Returns(newUser);
|
||||||
|
|
||||||
var user = await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel);
|
var user = await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).InviteUserAsync(organizationId, EventSystemUser.SCIM, scimUserRequestModel.PrimaryEmail.ToLowerInvariant(),
|
await sutProvider.GetDependency<IOrganizationService>().Received(1).InviteUserAsync(organizationId, EventSystemUser.SCIM, scimUserRequestModel.PrimaryEmail.ToLowerInvariant(),
|
||||||
OrganizationUserType.User, false, scimUserRequestModel.ExternalId, Arg.Any<List<CollectionAccessSelection>>(), Arg.Any<List<Guid>>());
|
OrganizationUserType.User, false, scimUserRequestModel.ExternalId, Arg.Any<List<CollectionAccessSelection>>(), Arg.Any<List<Guid>>(), true);
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByIdAsync(newUser.Id);
|
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByIdAsync(newUser.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,6 +269,35 @@ public class OrganizationsController : Controller
|
|||||||
return RedirectToAction("Index");
|
return RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
[RequirePermission(Permission.Org_Delete)]
|
||||||
|
public async Task<IActionResult> DeleteInitiation(Guid id, OrganizationInitiateDeleteModel model)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
TempData["Error"] = ModelState.GetErrorMessage();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||||
|
if (organization != null)
|
||||||
|
{
|
||||||
|
await _organizationService.InitiateDeleteAsync(organization, model.AdminEmail);
|
||||||
|
TempData["Success"] = "The request to initiate deletion of the organization has been sent.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
TempData["Error"] = ex.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction("Edit", new { id });
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> TriggerBillingSync(Guid id)
|
public async Task<IActionResult> TriggerBillingSync(Guid id)
|
||||||
{
|
{
|
||||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||||
@ -349,7 +378,10 @@ public class OrganizationsController : Controller
|
|||||||
providerOrganization,
|
providerOrganization,
|
||||||
organization);
|
organization);
|
||||||
|
|
||||||
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
|
if (organization.IsStripeEnabled())
|
||||||
|
{
|
||||||
|
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
|
||||||
|
}
|
||||||
|
|
||||||
return Json(null);
|
return Json(null);
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ using Bit.Admin.Utilities;
|
|||||||
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.Billing.Commands;
|
using Bit.Core.Billing.Commands;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
@ -69,7 +70,10 @@ public class ProviderOrganizationsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
|
if (organization.IsStripeEnabled())
|
||||||
|
{
|
||||||
|
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
|
||||||
|
}
|
||||||
|
|
||||||
return Json(null);
|
return Json(null);
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,6 @@ 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.Tools.Services;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -36,7 +35,6 @@ public class ProvidersController : Controller
|
|||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
private readonly IProviderService _providerService;
|
private readonly IProviderService _providerService;
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly ICreateProviderCommand _createProviderCommand;
|
private readonly ICreateProviderCommand _createProviderCommand;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
@ -51,7 +49,6 @@ public class ProvidersController : Controller
|
|||||||
IProviderService providerService,
|
IProviderService providerService,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
IReferenceEventService referenceEventService,
|
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
ICreateProviderCommand createProviderCommand,
|
ICreateProviderCommand createProviderCommand,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
@ -65,7 +62,6 @@ public class ProvidersController : Controller
|
|||||||
_providerService = providerService;
|
_providerService = providerService;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_referenceEventService = referenceEventService;
|
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_createProviderCommand = createProviderCommand;
|
_createProviderCommand = createProviderCommand;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
@ -104,8 +100,8 @@ public class ProvidersController : Controller
|
|||||||
return View(new CreateProviderModel
|
return View(new CreateProviderModel
|
||||||
{
|
{
|
||||||
OwnerEmail = ownerEmail,
|
OwnerEmail = ownerEmail,
|
||||||
TeamsMinimumSeats = teamsMinimumSeats,
|
TeamsMonthlySeatMinimum = teamsMinimumSeats,
|
||||||
EnterpriseMinimumSeats = enterpriseMinimumSeats
|
EnterpriseMonthlySeatMinimum = enterpriseMinimumSeats
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,8 +119,11 @@ public class ProvidersController : Controller
|
|||||||
switch (provider.Type)
|
switch (provider.Type)
|
||||||
{
|
{
|
||||||
case ProviderType.Msp:
|
case ProviderType.Msp:
|
||||||
await _createProviderCommand.CreateMspAsync(provider, model.OwnerEmail, model.TeamsMinimumSeats,
|
await _createProviderCommand.CreateMspAsync(
|
||||||
model.EnterpriseMinimumSeats);
|
provider,
|
||||||
|
model.OwnerEmail,
|
||||||
|
model.TeamsMonthlySeatMinimum,
|
||||||
|
model.EnterpriseMonthlySeatMinimum);
|
||||||
break;
|
break;
|
||||||
case ProviderType.Reseller:
|
case ProviderType.Reseller:
|
||||||
await _createProviderCommand.CreateResellerAsync(provider);
|
await _createProviderCommand.CreateResellerAsync(provider);
|
||||||
@ -167,8 +166,9 @@ public class ProvidersController : Controller
|
|||||||
return View(new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>()));
|
return View(new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>()));
|
||||||
}
|
}
|
||||||
|
|
||||||
var providerPlan = await _providerPlanRepository.GetByProviderId(id);
|
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
||||||
return View(new ProviderEditModel(provider, users, providerOrganizations, providerPlan));
|
|
||||||
|
return View(new ProviderEditModel(provider, users, providerOrganizations, providerPlans.ToList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@ -177,14 +177,15 @@ 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 providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
|
||||||
var provider = await _providerRepository.GetByIdAsync(id);
|
var provider = await _providerRepository.GetByIdAsync(id);
|
||||||
|
|
||||||
if (provider == null)
|
if (provider == null)
|
||||||
{
|
{
|
||||||
return RedirectToAction("Index");
|
return RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
||||||
model.ToProvider(provider);
|
model.ToProvider(provider);
|
||||||
|
|
||||||
await _providerRepository.ReplaceAsync(provider);
|
await _providerRepository.ReplaceAsync(provider);
|
||||||
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
|
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
|
||||||
|
|
||||||
@ -195,13 +196,14 @@ public class ProvidersController : Controller
|
|||||||
return RedirectToAction("Edit", new { id });
|
return RedirectToAction("Edit", new { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
model.ToProviderPlan(providerPlans);
|
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
||||||
|
|
||||||
if (providerPlans.Count == 0)
|
if (providerPlans.Count == 0)
|
||||||
{
|
{
|
||||||
var newProviderPlans = new List<ProviderPlan>
|
var newProviderPlans = new List<ProviderPlan>
|
||||||
{
|
{
|
||||||
new() {ProviderId = provider.Id, PlanType = PlanType.TeamsMonthly, SeatMinimum= model.TeamsMinimumSeats, PurchasedSeats = 0, AllocatedSeats = 0},
|
new () { ProviderId = provider.Id, PlanType = PlanType.TeamsMonthly, SeatMinimum = model.TeamsMonthlySeatMinimum, PurchasedSeats = 0, AllocatedSeats = 0 },
|
||||||
new() {ProviderId = provider.Id, PlanType = PlanType.EnterpriseMonthly, SeatMinimum= model.EnterpriseMinimumSeats, PurchasedSeats = 0, AllocatedSeats = 0}
|
new () { ProviderId = provider.Id, PlanType = PlanType.EnterpriseMonthly, SeatMinimum = model.EnterpriseMonthlySeatMinimum, PurchasedSeats = 0, AllocatedSeats = 0 }
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var newProviderPlan in newProviderPlans)
|
foreach (var newProviderPlan in newProviderPlans)
|
||||||
@ -213,6 +215,15 @@ public class ProvidersController : Controller
|
|||||||
{
|
{
|
||||||
foreach (var providerPlan in providerPlans)
|
foreach (var providerPlan in providerPlans)
|
||||||
{
|
{
|
||||||
|
if (providerPlan.PlanType == PlanType.EnterpriseMonthly)
|
||||||
|
{
|
||||||
|
providerPlan.SeatMinimum = model.EnterpriseMonthlySeatMinimum;
|
||||||
|
}
|
||||||
|
else if (providerPlan.PlanType == PlanType.TeamsMonthly)
|
||||||
|
{
|
||||||
|
providerPlan.SeatMinimum = model.TeamsMonthlySeatMinimum;
|
||||||
|
}
|
||||||
|
|
||||||
await _providerPlanRepository.ReplaceAsync(providerPlan);
|
await _providerPlanRepository.ReplaceAsync(providerPlan);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -294,9 +305,8 @@ public class ProvidersController : Controller
|
|||||||
return RedirectToAction("Index");
|
return RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
||||||
var flexibleCollectionsSignupEnabled = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsSignup);
|
|
||||||
var flexibleCollectionsV1Enabled = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1);
|
var flexibleCollectionsV1Enabled = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1);
|
||||||
var organization = model.CreateOrganization(provider, flexibleCollectionsSignupEnabled, flexibleCollectionsV1Enabled);
|
var organization = model.CreateOrganization(provider, flexibleCollectionsV1Enabled);
|
||||||
await _organizationService.CreatePendingOrganization(organization, model.Owners, User, _userService, model.SalesAssistedTrialStarted);
|
await _organizationService.CreatePendingOrganization(organization, model.Owners, User, _userService, model.SalesAssistedTrialStarted);
|
||||||
await _providerService.AddOrganization(providerId, organization.Id, null);
|
await _providerService.AddOrganization(providerId, organization.Id, null);
|
||||||
|
|
||||||
|
@ -24,11 +24,11 @@ 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")]
|
[Display(Name = "Teams (Monthly) Seat Minimum")]
|
||||||
public int TeamsMinimumSeats { get; set; }
|
public int TeamsMonthlySeatMinimum { get; set; }
|
||||||
|
|
||||||
[Display(Name = "Enterprise minimum seats")]
|
[Display(Name = "Enterprise (Monthly) Seat Minimum")]
|
||||||
public int EnterpriseMinimumSeats { get; set; }
|
public int EnterpriseMonthlySeatMinimum { get; set; }
|
||||||
|
|
||||||
public virtual Provider ToProvider()
|
public virtual Provider ToProvider()
|
||||||
{
|
{
|
||||||
@ -51,14 +51,14 @@ 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)
|
if (TeamsMonthlySeatMinimum < 0)
|
||||||
{
|
{
|
||||||
var teamsMinimumSeatsDisplayName = nameof(TeamsMinimumSeats).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(TeamsMinimumSeats);
|
var teamsMinimumSeatsDisplayName = nameof(TeamsMonthlySeatMinimum).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(TeamsMonthlySeatMinimum);
|
||||||
yield return new ValidationResult($"The {teamsMinimumSeatsDisplayName} field can not be negative.");
|
yield return new ValidationResult($"The {teamsMinimumSeatsDisplayName} field can not be negative.");
|
||||||
}
|
}
|
||||||
if (EnterpriseMinimumSeats < 0)
|
if (EnterpriseMonthlySeatMinimum < 0)
|
||||||
{
|
{
|
||||||
var enterpriseMinimumSeatsDisplayName = nameof(EnterpriseMinimumSeats).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMinimumSeats);
|
var enterpriseMinimumSeatsDisplayName = nameof(EnterpriseMonthlySeatMinimum).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMonthlySeatMinimum);
|
||||||
yield return new ValidationResult($"The {enterpriseMinimumSeatsDisplayName} field can not be negative.");
|
yield return new ValidationResult($"The {enterpriseMinimumSeatsDisplayName} field can not be negative.");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -162,19 +162,18 @@ public class OrganizationEditModel : OrganizationViewModel
|
|||||||
{ "baseServiceAccount", p.SecretsManager.BaseServiceAccount }
|
{ "baseServiceAccount", p.SecretsManager.BaseServiceAccount }
|
||||||
});
|
});
|
||||||
|
|
||||||
public Organization CreateOrganization(Provider provider, bool flexibleCollectionsSignupEnabled, bool flexibleCollectionsV1Enabled)
|
public Organization CreateOrganization(Provider provider, bool flexibleCollectionsV1Enabled)
|
||||||
{
|
{
|
||||||
BillingEmail = provider.BillingEmail;
|
BillingEmail = provider.BillingEmail;
|
||||||
|
|
||||||
var newOrg = new Organization
|
var newOrg = new Organization
|
||||||
{
|
{
|
||||||
// This feature flag indicates that new organizations should be automatically onboarded to
|
// Flexible Collections MVP is fully released and all organizations must always have this setting enabled.
|
||||||
// Flexible Collections enhancements
|
// AC-1714 will remove this flag after all old code has been removed.
|
||||||
FlexibleCollections = flexibleCollectionsSignupEnabled,
|
FlexibleCollections = true,
|
||||||
// These collection management settings smooth the migration for existing organizations by disabling some FC behavior.
|
|
||||||
// If the organization is onboarded to Flexible Collections on signup, we turn them OFF to enable all new behaviour.
|
// This is a transitional setting that defaults to ON until Flexible Collections v1 is released
|
||||||
// If the organization is NOT onboarded now, they will have to be migrated later, so they default to ON to limit FC changes on migration.
|
// (to preserve existing behavior) and defaults to OFF after release (enabling new behavior)
|
||||||
LimitCollectionCreationDeletion = !flexibleCollectionsSignupEnabled,
|
|
||||||
AllowAdminAccessToAllCollectionItems = !flexibleCollectionsV1Enabled
|
AllowAdminAccessToAllCollectionItems = !flexibleCollectionsV1Enabled
|
||||||
};
|
};
|
||||||
return ToOrganization(newOrg);
|
return ToOrganization(newOrg);
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Bit.Admin.AdminConsole.Models;
|
||||||
|
|
||||||
|
public class OrganizationInitiateDeleteModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
[StringLength(256)]
|
||||||
|
[Display(Name = "Admin Email")]
|
||||||
|
public string AdminEmail { get; set; }
|
||||||
|
}
|
@ -10,16 +10,21 @@ public class ProviderEditModel : ProviderViewModel
|
|||||||
{
|
{
|
||||||
public ProviderEditModel() { }
|
public ProviderEditModel() { }
|
||||||
|
|
||||||
public ProviderEditModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers,
|
public ProviderEditModel(
|
||||||
IEnumerable<ProviderOrganizationOrganizationDetails> organizations, IEnumerable<ProviderPlan> providerPlans)
|
Provider provider,
|
||||||
: base(provider, providerUsers, organizations)
|
IEnumerable<ProviderUserUserDetails> providerUsers,
|
||||||
|
IEnumerable<ProviderOrganizationOrganizationDetails> organizations,
|
||||||
|
IReadOnlyCollection<ProviderPlan> providerPlans) : 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);
|
TeamsMonthlySeatMinimum = GetSeatMinimum(providerPlans, PlanType.TeamsMonthly);
|
||||||
EnterpriseMinimumSeats = GetMinimumSeats(providerPlans, PlanType.EnterpriseMonthly);
|
EnterpriseMonthlySeatMinimum = GetSeatMinimum(providerPlans, PlanType.EnterpriseMonthly);
|
||||||
|
Gateway = provider.Gateway;
|
||||||
|
GatewayCustomerId = provider.GatewayCustomerId;
|
||||||
|
GatewaySubscriptionId = provider.GatewaySubscriptionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Display(Name = "Billing Email")]
|
[Display(Name = "Billing Email")]
|
||||||
@ -29,38 +34,28 @@ 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")]
|
[Display(Name = "Teams (Monthly) Seat Minimum")]
|
||||||
public int TeamsMinimumSeats { get; set; }
|
public int TeamsMonthlySeatMinimum { get; set; }
|
||||||
|
|
||||||
[Display(Name = "Enterprise minimum seats")]
|
[Display(Name = "Enterprise (Monthly) Seat Minimum")]
|
||||||
public int EnterpriseMinimumSeats { get; set; }
|
public int EnterpriseMonthlySeatMinimum { get; set; }
|
||||||
[Display(Name = "Events")]
|
[Display(Name = "Gateway")]
|
||||||
|
public GatewayType? Gateway { get; set; }
|
||||||
|
[Display(Name = "Gateway Customer Id")]
|
||||||
|
public string GatewayCustomerId { get; set; }
|
||||||
|
[Display(Name = "Gateway Subscription Id")]
|
||||||
|
public string GatewaySubscriptionId { get; set; }
|
||||||
|
|
||||||
public IEnumerable<ProviderPlan> ToProviderPlan(IEnumerable<ProviderPlan> existingProviderPlans)
|
public virtual Provider ToProvider(Provider existingProvider)
|
||||||
{
|
{
|
||||||
var providerPlans = existingProviderPlans.ToList();
|
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant().Trim();
|
||||||
foreach (var existingProviderPlan in providerPlans)
|
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant().Trim();
|
||||||
{
|
existingProvider.Gateway = Gateway;
|
||||||
existingProviderPlan.SeatMinimum = existingProviderPlan.PlanType switch
|
existingProvider.GatewayCustomerId = GatewayCustomerId;
|
||||||
{
|
existingProvider.GatewaySubscriptionId = GatewaySubscriptionId;
|
||||||
PlanType.TeamsMonthly => TeamsMinimumSeats,
|
|
||||||
PlanType.EnterpriseMonthly => EnterpriseMinimumSeats,
|
|
||||||
_ => existingProviderPlan.SeatMinimum
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return providerPlans;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Provider ToProvider(Provider existingProvider)
|
|
||||||
{
|
|
||||||
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
|
|
||||||
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant()?.Trim();
|
|
||||||
return existingProvider;
|
return existingProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int GetSeatMinimum(IEnumerable<ProviderPlan> providerPlans, PlanType planType)
|
||||||
private int GetMinimumSeats(IEnumerable<ProviderPlan> providerPlans, PlanType planType)
|
=> providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == planType)?.SeatMinimum ?? 0;
|
||||||
{
|
|
||||||
return (from providerPlan in providerPlans where providerPlan.PlanType == planType select (int)providerPlan.SeatMinimum).FirstOrDefault();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@using Bit.Admin.Enums;
|
@using Bit.Admin.Enums;
|
||||||
@using Bit.Admin.Models
|
@using Bit.Admin.Models
|
||||||
@using Bit.Core.Enums
|
@using Bit.Core.Enums
|
||||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||||
@ -18,24 +18,43 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
(() => {
|
(() => {
|
||||||
document.getElementById('teams-trial').addEventListener('click', () => {
|
const treamsTrialButton = document.getElementById('teams-trial');
|
||||||
if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)PlanType.Free)') {
|
if (treamsTrialButton != null) {
|
||||||
alert('Organization is not on a free plan.');
|
treamsTrialButton.addEventListener('click', () => {
|
||||||
return;
|
if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)PlanType.Free)') {
|
||||||
}
|
alert('Organization is not on a free plan.');
|
||||||
setTrialDefaults('@((byte)PlanType.TeamsAnnually)');
|
return;
|
||||||
togglePlanFeatures('@((byte)PlanType.TeamsAnnually)');
|
}
|
||||||
document.getElementById('@(nameof(Model.Plan))').value = 'Teams (Trial)';
|
setTrialDefaults('@((byte)PlanType.TeamsAnnually)');
|
||||||
});
|
togglePlanFeatures('@((byte)PlanType.TeamsAnnually)');
|
||||||
document.getElementById('enterprise-trial').addEventListener('click', () => {
|
document.getElementById('@(nameof(Model.Plan))').value = 'Teams (Trial)';
|
||||||
if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)PlanType.Free)') {
|
});
|
||||||
alert('Organization is not on a free plan.');
|
}
|
||||||
return;
|
|
||||||
}
|
const entTrialButton = document.getElementById('enterprise-trial');
|
||||||
setTrialDefaults('@((byte)PlanType.EnterpriseAnnually)');
|
if (entTrialButton != null) {
|
||||||
togglePlanFeatures('@((byte)PlanType.EnterpriseAnnually)');
|
entTrialButton.addEventListener('click', () => {
|
||||||
document.getElementById('@(nameof(Model.Plan))').value = 'Enterprise (Trial)';
|
if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)PlanType.Free)') {
|
||||||
});
|
alert('Organization is not on a free plan.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTrialDefaults('@((byte)PlanType.EnterpriseAnnually)');
|
||||||
|
togglePlanFeatures('@((byte)PlanType.EnterpriseAnnually)');
|
||||||
|
document.getElementById('@(nameof(Model.Plan))').value = 'Enterprise (Trial)';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const initDeleteButton = document.getElementById('initiate-delete-form');
|
||||||
|
if (initDeleteButton != null) {
|
||||||
|
initDeleteButton.addEventListener('submit', (e) => {
|
||||||
|
const email = prompt('Enter the email address of the owner/admin that your want to ' +
|
||||||
|
'request the organization delete verification process with.');
|
||||||
|
document.getElementById('AdminEmail').value = email;
|
||||||
|
if (email == null || email === '') {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function setTrialDefaults(planType) {
|
function setTrialDefaults(planType) {
|
||||||
// Plan
|
// Plan
|
||||||
@ -76,7 +95,7 @@
|
|||||||
{
|
{
|
||||||
<h2>Billing Information</h2>
|
<h2>Billing Information</h2>
|
||||||
@await Html.PartialAsync("_BillingInformation",
|
@await Html.PartialAsync("_BillingInformation",
|
||||||
new BillingInformationModel { BillingInfo = Model.BillingInfo, OrganizationId = Model.Organization.Id, Entity = "Organization" })
|
new BillingInformationModel { BillingInfo = Model.BillingInfo, OrganizationId = Model.Organization.Id, Entity = "Organization" })
|
||||||
}
|
}
|
||||||
|
|
||||||
@await Html.PartialAsync("~/AdminConsole/Views/Shared/_OrganizationForm.cshtml", Model)
|
@await Html.PartialAsync("~/AdminConsole/Views/Shared/_OrganizationForm.cshtml", Model)
|
||||||
@ -95,18 +114,20 @@
|
|||||||
}
|
}
|
||||||
@if (canUnlinkFromProvider && Model.Provider is not null)
|
@if (canUnlinkFromProvider && Model.Provider is not null)
|
||||||
{
|
{
|
||||||
<button
|
<button class="btn btn-outline-danger mr-2"
|
||||||
class="btn btn-outline-danger mr-2"
|
onclick="return unlinkProvider('@Model.Organization.Id');">
|
||||||
onclick="return unlinkProvider('@Model.Organization.Id');"
|
|
||||||
>
|
|
||||||
Unlink provider
|
Unlink provider
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (canDelete)
|
@if (canDelete)
|
||||||
{
|
{
|
||||||
|
<form asp-action="DeleteInitiation" asp-route-id="@Model.Organization.Id" id="initiate-delete-form">
|
||||||
|
<input type="hidden" name="AdminEmail" id="AdminEmail" />
|
||||||
|
<button class="btn btn-danger mr-2" type="submit">Request Delete</button>
|
||||||
|
</form>
|
||||||
<form asp-action="Delete" asp-route-id="@Model.Organization.Id"
|
<form asp-action="Delete" asp-route-id="@Model.Organization.Id"
|
||||||
onsubmit="return confirm('Are you sure you want to delete this organization?')">
|
onsubmit="return confirm('Are you sure you want to hard delete this organization?')">
|
||||||
<button class="btn btn-danger" type="submit">Delete</button>
|
<button class="btn btn-outline-danger" type="submit">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,14 +46,14 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="TeamsMinimumSeats"></label>
|
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
||||||
<input type="number" class="form-control" asp-for="TeamsMinimumSeats">
|
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="EnterpriseMinimumSeats"></label>
|
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
||||||
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
|
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,103 +48,142 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="TeamsMinimumSeats"></label>
|
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
||||||
<input type="number" class="form-control" asp-for="TeamsMinimumSeats">
|
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="EnterpriseMinimumSeats"></label>
|
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
||||||
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
|
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Gateway"></label>
|
||||||
|
<select class="form-control" asp-for="Gateway" asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||||
|
<option value="">--</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="GatewayCustomerId"></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" asp-for="GatewayCustomerId">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-secondary" type="button" id="gateway-customer-link">
|
||||||
|
<i class="fa fa-external-link"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="GatewaySubscriptionId"></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" asp-for="GatewaySubscriptionId">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
|
||||||
|
<i class="fa fa-external-link"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
@await Html.PartialAsync("Organizations", Model)
|
@await Html.PartialAsync("Organizations", Model)
|
||||||
@if (canEdit)
|
@if (canEdit)
|
||||||
{
|
{
|
||||||
<!-- Modals -->
|
<!-- Modals -->
|
||||||
<div class="modal fade rounded" id="requestDeletionModal" tabindex="-1" aria-labelledby="requestDeletionModal" aria-hidden="true">
|
<div class="modal fade rounded" id="requestDeletionModal" tabindex="-1" aria-labelledby="requestDeletionModal" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content rounded">
|
<div class="modal-content rounded">
|
||||||
<div class="p-3">
|
<div class="p-3">
|
||||||
<h4 class="font-weight-bolder" id="exampleModalLabel">Request provider deletion</h4>
|
<h4 class="font-weight-bolder" id="exampleModalLabel">Request provider deletion</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<span class="font-weight-light">
|
<span class="font-weight-light">
|
||||||
Enter the email of the provider admin that will receive the request to delete the provider portal.
|
Enter the email of the provider admin that will receive the request to delete the provider portal.
|
||||||
</span>
|
</span>
|
||||||
<form>
|
<form>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="provider-email" class="col-form-label">Provider email</label>
|
<label for="provider-email" class="col-form-label">Provider email</label>
|
||||||
<input type="email" class="form-control" id="provider-email">
|
<input type="email" class="form-control" id="provider-email">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
|
||||||
<button type="button" class="btn btn-danger btn-pill" onclick="initiateDeleteProvider('@Model.Provider.Id')">Send email request</button>
|
<button type="button" class="btn btn-danger btn-pill" onclick="initiateDeleteProvider('@Model.Provider.Id')">Send email request</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal fade" id="DeleteModal" tabindex="-1" aria-labelledby="DeleteModal" aria-hidden="true">
|
<div class="modal fade" id="DeleteModal" tabindex="-1" aria-labelledby="DeleteModal" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content rounded">
|
<div class="modal-content rounded">
|
||||||
<div class="p-3">
|
<div class="p-3">
|
||||||
<h4 class="font-weight-bolder" id="exampleModalLabel">Delete provider</h4>
|
<h4 class="font-weight-bolder" id="exampleModalLabel">Delete provider</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<span class="font-weight-light">
|
<span class="font-weight-light">
|
||||||
This action is permanent and irreversible. Enter the provider name to complete deletion of the provider and associated data.
|
This action is permanent and irreversible. Enter the provider name to complete deletion of the provider and associated data.
|
||||||
</span>
|
</span>
|
||||||
<form>
|
<form>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="provider-name" class="col-form-label">Provider name</label>
|
<label for="provider-name" class="col-form-label">Provider name</label>
|
||||||
<input type="text" class="form-control" id="provider-name">
|
<input type="text" class="form-control" id="provider-name">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
|
||||||
<button type="button" class="btn btn-danger btn-pill" onclick="deleteProvider('@Model.Provider.Id');">Delete provider</button>
|
<button type="button" class="btn btn-danger btn-pill" onclick="deleteProvider('@Model.Provider.Id');">Delete provider</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal fade" id="linkedWarningModal" tabindex="-1" role="dialog" aria-labelledby="linkedWarningModal" aria-hidden="true">
|
<div class="modal fade" id="linkedWarningModal" tabindex="-1" role="dialog" aria-labelledby="linkedWarningModal" aria-hidden="true">
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<div class="modal-content rounded">
|
<div class="modal-content rounded">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<h4 class="font-weight-bolder">Cannot Delete @Model.Name</h4>
|
<h4 class="font-weight-bolder">Cannot Delete @Model.Name</h4>
|
||||||
<p class="font-weight-lighter">You must unlink all clients before you can delete @Model.Name.</p>
|
<p class="font-weight-lighter">You must unlink all clients before you can delete @Model.Name.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
|
||||||
<button type="button" class="btn btn-primary btn-pill" data-dismiss="modal">Ok</button>
|
<button type="button" class="btn btn-primary btn-pill" data-dismiss="modal">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- End of Modal Section -->
|
<!-- End of Modal Section -->
|
||||||
|
|
||||||
<div class="d-flex mt-4">
|
<div class="d-flex mt-4">
|
||||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
||||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableDeleteProvider))
|
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableDeleteProvider))
|
||||||
{
|
{
|
||||||
<div class="ml-auto d-flex">
|
<div class="ml-auto d-flex">
|
||||||
<button class="btn btn-danger" onclick="openRequestDeleteModal(@Model.ProviderOrganizations.Count())">Request Delete</button>
|
<button class="btn btn-danger" onclick="openRequestDeleteModal(@Model.ProviderOrganizations.Count())">Request Delete</button>
|
||||||
<button id="requestDeletionBtn" hidden="hidden" data-toggle="modal" data-target="#requestDeletionModal"></button>
|
<button id="requestDeletionBtn" hidden="hidden" data-toggle="modal" data-target="#requestDeletionModal"></button>
|
||||||
|
|
||||||
<button class="btn btn-outline-danger ml-2" onclick="openDeleteModal(@Model.ProviderOrganizations.Count())">Delete</button>
|
<button class="btn btn-outline-danger ml-2" onclick="openDeleteModal(@Model.ProviderOrganizations.Count())">Delete</button>
|
||||||
<button id="deleteBtn" hidden="hidden" data-toggle="modal" data-target="#DeleteModal"></button>
|
<button id="deleteBtn" hidden="hidden" data-toggle="modal" data-target="#DeleteModal"></button>
|
||||||
|
|
||||||
<button id="linkAccWarningBtn" hidden="hidden" data-toggle="modal" data-target="#linkedWarningModal"></button>
|
<button id="linkAccWarningBtn" hidden="hidden" data-toggle="modal" data-target="#linkedWarningModal"></button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
}
|
|
||||||
|
@ -70,9 +70,10 @@
|
|||||||
@{
|
@{
|
||||||
var planTypes = Enum.GetValues<PlanType>()
|
var planTypes = Enum.GetValues<PlanType>()
|
||||||
.Where(p =>
|
.Where(p =>
|
||||||
Model.Provider == null ||
|
(Model.Provider == null ||
|
||||||
(Model.Provider != null
|
p is >= PlanType.TeamsMonthly2019 and <= PlanType.EnterpriseAnnually2019 or
|
||||||
&& p is >= PlanType.TeamsMonthly2019 and <= PlanType.EnterpriseAnnually2019 or >= PlanType.TeamsMonthly2020 and <= PlanType.EnterpriseAnnually)
|
>= PlanType.TeamsMonthly2020 and <= PlanType.EnterpriseAnnually) &&
|
||||||
|
p != PlanType.TeamsStarter
|
||||||
)
|
)
|
||||||
.Select(e => new SelectListItem
|
.Select(e => new SelectListItem
|
||||||
{
|
{
|
||||||
|
@ -174,6 +174,14 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
|
@if (TempData["Success"] != null)
|
||||||
|
{
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
toastr.success("@TempData["Success"]")
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
|
||||||
@RenderSection("Scripts", required: false)
|
@RenderSection("Scripts", required: false)
|
||||||
</body>
|
</body>
|
||||||
|
@ -109,7 +109,7 @@
|
|||||||
<option asp-selected="Model.Filter.Price == null" value="@null">All</option>
|
<option asp-selected="Model.Filter.Price == null" value="@null">All</option>
|
||||||
@foreach (var price in Model.Prices)
|
@foreach (var price in Model.Prices)
|
||||||
{
|
{
|
||||||
<option asp-selected='@Model.Filter.Price == @price.Id' value="@price.Id">@price.Id</option>
|
<option asp-selected='@(Model.Filter.Price == price.Id)' value="@price.Id">@price.Id</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -119,7 +119,7 @@
|
|||||||
<option asp-selected="Model.Filter.TestClock == null" value="@null">All</option>
|
<option asp-selected="Model.Filter.TestClock == null" value="@null">All</option>
|
||||||
@foreach (var clock in Model.TestClocks)
|
@foreach (var clock in Model.TestClocks)
|
||||||
{
|
{
|
||||||
<option asp-selected='@Model.Filter.TestClock == @clock.Id' value="@clock.Id">@clock.Name</option>
|
<option asp-selected='@(Model.Filter.TestClock == clock.Id)' value="@clock.Id">@clock.Name</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -278,4 +278,4 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</nav>
|
</nav>
|
||||||
</form>
|
</form>
|
||||||
|
@ -137,7 +137,9 @@ public class GroupsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Flexible Collections - check the user has permission to grant access to the collections for the new group
|
// Flexible Collections - check the user has permission to grant access to the collections for the new group
|
||||||
if (await FlexibleCollectionsIsEnabledAsync(orgId) && _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1))
|
if (await FlexibleCollectionsIsEnabledAsync(orgId) &&
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1) &&
|
||||||
|
model.Collections?.Any() == true)
|
||||||
{
|
{
|
||||||
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Collections.Select(a => a.Id));
|
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Collections.Select(a => a.Id));
|
||||||
var authorized =
|
var authorized =
|
||||||
@ -198,7 +200,8 @@ public class GroupsController : Controller
|
|||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(orgId, userId);
|
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(orgId, userId);
|
||||||
var currentGroupUsers = await _groupRepository.GetManyUserIdsByIdAsync(id);
|
var currentGroupUsers = await _groupRepository.GetManyUserIdsByIdAsync(id);
|
||||||
if (!currentGroupUsers.Contains(organizationUser.Id) && model.Users.Contains(organizationUser.Id))
|
// OrganizationUser may be null if the current user is a provider
|
||||||
|
if (organizationUser != null && !currentGroupUsers.Contains(organizationUser.Id) && model.Users.Contains(organizationUser.Id))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("You cannot add yourself to groups.");
|
throw new BadRequestException("You cannot add yourself to groups.");
|
||||||
}
|
}
|
||||||
|
@ -195,7 +195,9 @@ public class OrganizationUsersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Flexible Collections - check the user has permission to grant access to the collections for the new user
|
// Flexible Collections - check the user has permission to grant access to the collections for the new user
|
||||||
if (await FlexibleCollectionsIsEnabledAsync(orgId) && _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1))
|
if (await FlexibleCollectionsIsEnabledAsync(orgId) &&
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1) &&
|
||||||
|
model.Collections?.Any() == true)
|
||||||
{
|
{
|
||||||
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Collections.Select(a => a.Id));
|
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Collections.Select(a => a.Id));
|
||||||
var authorized =
|
var authorized =
|
||||||
|
@ -6,12 +6,12 @@ using Bit.Api.AdminConsole.Models.Response.Organizations;
|
|||||||
using Bit.Api.Auth.Models.Request.Accounts;
|
using Bit.Api.Auth.Models.Request.Accounts;
|
||||||
using Bit.Api.Auth.Models.Request.Organizations;
|
using Bit.Api.Auth.Models.Request.Organizations;
|
||||||
using Bit.Api.Auth.Models.Response.Organizations;
|
using Bit.Api.Auth.Models.Response.Organizations;
|
||||||
using Bit.Api.Models.Request;
|
|
||||||
using Bit.Api.Models.Request.Accounts;
|
using Bit.Api.Models.Request.Accounts;
|
||||||
using Bit.Api.Models.Request.Organizations;
|
using Bit.Api.Models.Request.Organizations;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces;
|
||||||
@ -21,20 +21,13 @@ using Bit.Core.Auth.Repositories;
|
|||||||
using Bit.Core.Auth.Services;
|
using Bit.Core.Auth.Services;
|
||||||
using Bit.Core.Billing.Commands;
|
using Bit.Core.Billing.Commands;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Models;
|
|
||||||
using Bit.Core.Billing.Queries;
|
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Business;
|
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
|
||||||
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.Tools.Enums;
|
using Bit.Core.Tokens;
|
||||||
using Bit.Core.Tools.Models.Business;
|
|
||||||
using Bit.Core.Tools.Services;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -50,7 +43,6 @@ public class OrganizationsController : Controller
|
|||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyRepository _policyRepository;
|
||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IPaymentService _paymentService;
|
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||||
private readonly ISsoConfigService _ssoConfigService;
|
private readonly ISsoConfigService _ssoConfigService;
|
||||||
@ -58,20 +50,13 @@ public class OrganizationsController : Controller
|
|||||||
private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;
|
private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;
|
||||||
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
|
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
|
||||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||||
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
|
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly ILicensingService _licensingService;
|
|
||||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
|
||||||
private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand;
|
|
||||||
private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand;
|
|
||||||
private readonly IPushNotificationService _pushNotificationService;
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
private readonly ICancelSubscriptionCommand _cancelSubscriptionCommand;
|
|
||||||
private readonly ISubscriberQueries _subscriberQueries;
|
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
|
||||||
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
|
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
|
||||||
private readonly IProviderRepository _providerRepository;
|
private readonly IProviderRepository _providerRepository;
|
||||||
private readonly IScaleSeatsCommand _scaleSeatsCommand;
|
private readonly IScaleSeatsCommand _scaleSeatsCommand;
|
||||||
|
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
|
||||||
|
|
||||||
public OrganizationsController(
|
public OrganizationsController(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@ -79,7 +64,6 @@ public class OrganizationsController : Controller
|
|||||||
IPolicyRepository policyRepository,
|
IPolicyRepository policyRepository,
|
||||||
IOrganizationService organizationService,
|
IOrganizationService organizationService,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IPaymentService paymentService,
|
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
ISsoConfigRepository ssoConfigRepository,
|
ISsoConfigRepository ssoConfigRepository,
|
||||||
ISsoConfigService ssoConfigService,
|
ISsoConfigService ssoConfigService,
|
||||||
@ -87,27 +71,19 @@ public class OrganizationsController : Controller
|
|||||||
IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand,
|
IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand,
|
||||||
ICreateOrganizationApiKeyCommand createOrganizationApiKeyCommand,
|
ICreateOrganizationApiKeyCommand createOrganizationApiKeyCommand,
|
||||||
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||||
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
|
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
ILicensingService licensingService,
|
|
||||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
|
||||||
IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand,
|
|
||||||
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
|
|
||||||
IPushNotificationService pushNotificationService,
|
IPushNotificationService pushNotificationService,
|
||||||
ICancelSubscriptionCommand cancelSubscriptionCommand,
|
|
||||||
ISubscriberQueries subscriberQueries,
|
|
||||||
IReferenceEventService referenceEventService,
|
|
||||||
IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand,
|
IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand,
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
IScaleSeatsCommand scaleSeatsCommand)
|
IScaleSeatsCommand scaleSeatsCommand,
|
||||||
|
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_policyRepository = policyRepository;
|
_policyRepository = policyRepository;
|
||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_paymentService = paymentService;
|
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_ssoConfigRepository = ssoConfigRepository;
|
_ssoConfigRepository = ssoConfigRepository;
|
||||||
_ssoConfigService = ssoConfigService;
|
_ssoConfigService = ssoConfigService;
|
||||||
@ -115,20 +91,13 @@ public class OrganizationsController : Controller
|
|||||||
_rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand;
|
_rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand;
|
||||||
_createOrganizationApiKeyCommand = createOrganizationApiKeyCommand;
|
_createOrganizationApiKeyCommand = createOrganizationApiKeyCommand;
|
||||||
_organizationApiKeyRepository = organizationApiKeyRepository;
|
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||||
_cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery;
|
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_licensingService = licensingService;
|
|
||||||
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
|
||||||
_upgradeOrganizationPlanCommand = upgradeOrganizationPlanCommand;
|
|
||||||
_addSecretsManagerSubscriptionCommand = addSecretsManagerSubscriptionCommand;
|
|
||||||
_pushNotificationService = pushNotificationService;
|
_pushNotificationService = pushNotificationService;
|
||||||
_cancelSubscriptionCommand = cancelSubscriptionCommand;
|
|
||||||
_subscriberQueries = subscriberQueries;
|
|
||||||
_referenceEventService = referenceEventService;
|
|
||||||
_organizationEnableCollectionEnhancementsCommand = organizationEnableCollectionEnhancementsCommand;
|
_organizationEnableCollectionEnhancementsCommand = organizationEnableCollectionEnhancementsCommand;
|
||||||
_providerRepository = providerRepository;
|
_providerRepository = providerRepository;
|
||||||
_scaleSeatsCommand = scaleSeatsCommand;
|
_scaleSeatsCommand = scaleSeatsCommand;
|
||||||
|
_orgDeleteTokenDataFactory = orgDeleteTokenDataFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
@ -149,83 +118,6 @@ public class OrganizationsController : Controller
|
|||||||
return new OrganizationResponseModel(organization);
|
return new OrganizationResponseModel(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/billing")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task<BillingResponseModel> GetBilling(string id)
|
|
||||||
{
|
|
||||||
var orgIdGuid = new Guid(id);
|
|
||||||
if (!await _currentContext.ViewBillingHistory(orgIdGuid))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var billingInfo = await _paymentService.GetBillingAsync(organization);
|
|
||||||
return new BillingResponseModel(billingInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}/subscription")]
|
|
||||||
public async Task<OrganizationSubscriptionResponseModel> GetSubscription(string id)
|
|
||||||
{
|
|
||||||
var orgIdGuid = new Guid(id);
|
|
||||||
if (!await _currentContext.ViewSubscription(orgIdGuid))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_globalSettings.SelfHosted && organization.Gateway != null)
|
|
||||||
{
|
|
||||||
var subscriptionInfo = await _paymentService.GetSubscriptionAsync(organization);
|
|
||||||
if (subscriptionInfo == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var hideSensitiveData = !await _currentContext.EditSubscription(orgIdGuid);
|
|
||||||
|
|
||||||
return new OrganizationSubscriptionResponseModel(organization, subscriptionInfo, hideSensitiveData);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_globalSettings.SelfHosted)
|
|
||||||
{
|
|
||||||
var orgLicense = await _licensingService.ReadOrganizationLicenseAsync(organization);
|
|
||||||
return new OrganizationSubscriptionResponseModel(organization, orgLicense);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new OrganizationSubscriptionResponseModel(organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}/license")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task<OrganizationLicense> GetLicense(string id, [FromQuery] Guid installationId)
|
|
||||||
{
|
|
||||||
var orgIdGuid = new Guid(id);
|
|
||||||
if (!await _currentContext.OrganizationOwner(orgIdGuid))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var org = await _organizationRepository.GetByIdAsync(new Guid(id));
|
|
||||||
var license = await _cloudGetOrganizationLicenseQuery.GetLicenseAsync(org, installationId);
|
|
||||||
if (license == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return license;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
public async Task<ListResponseModel<ProfileOrganizationResponseModel>> GetUser()
|
public async Task<ListResponseModel<ProfileOrganizationResponseModel>> GetUser()
|
||||||
{
|
{
|
||||||
@ -268,21 +160,6 @@ public class OrganizationsController : Controller
|
|||||||
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false);
|
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/billing-status")]
|
|
||||||
public async Task<OrganizationBillingStatusResponseModel> GetBillingStatus(Guid id)
|
|
||||||
{
|
|
||||||
if (!await _currentContext.EditPaymentMethods(id))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
|
||||||
|
|
||||||
var risksSubscriptionFailure = await _paymentService.RisksSubscriptionFailure(organization);
|
|
||||||
|
|
||||||
return new OrganizationBillingStatusResponseModel(organization, risksSubscriptionFailure);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("")]
|
[HttpPost("")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<OrganizationResponseModel> Post([FromBody] OrganizationCreateRequestModel model)
|
public async Task<OrganizationResponseModel> Post([FromBody] OrganizationCreateRequestModel model)
|
||||||
@ -326,124 +203,6 @@ public class OrganizationsController : Controller
|
|||||||
return new OrganizationResponseModel(organization);
|
return new OrganizationResponseModel(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/payment")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task PostPayment(string id, [FromBody] PaymentRequestModel model)
|
|
||||||
{
|
|
||||||
var orgIdGuid = new Guid(id);
|
|
||||||
if (!await _currentContext.EditPaymentMethods(orgIdGuid))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
await _organizationService.ReplacePaymentMethodAsync(orgIdGuid, model.PaymentToken,
|
|
||||||
model.PaymentMethodType.Value, new TaxInfo
|
|
||||||
{
|
|
||||||
BillingAddressLine1 = model.Line1,
|
|
||||||
BillingAddressLine2 = model.Line2,
|
|
||||||
BillingAddressState = model.State,
|
|
||||||
BillingAddressCity = model.City,
|
|
||||||
BillingAddressPostalCode = model.PostalCode,
|
|
||||||
BillingAddressCountry = model.Country,
|
|
||||||
TaxIdNumber = model.TaxId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/upgrade")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task<PaymentResponseModel> PostUpgrade(string id, [FromBody] OrganizationUpgradeRequestModel model)
|
|
||||||
{
|
|
||||||
var orgIdGuid = new Guid(id);
|
|
||||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var (success, paymentIntentClientSecret) = await _upgradeOrganizationPlanCommand.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade());
|
|
||||||
|
|
||||||
if (model.UseSecretsManager && success)
|
|
||||||
{
|
|
||||||
var userId = _userService.GetProperUserId(User).Value;
|
|
||||||
|
|
||||||
await TryGrantOwnerAccessToSecretsManagerAsync(orgIdGuid, userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new PaymentResponseModel { Success = success, PaymentIntentClientSecret = paymentIntentClientSecret };
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/subscription")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task PostSubscription(string id, [FromBody] OrganizationSubscriptionUpdateRequestModel model)
|
|
||||||
{
|
|
||||||
var orgIdGuid = new Guid(id);
|
|
||||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
await _organizationService.UpdateSubscription(orgIdGuid, model.SeatAdjustment, model.MaxAutoscaleSeats);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/sm-subscription")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task PostSmSubscription(Guid id, [FromBody] SecretsManagerSubscriptionUpdateRequestModel model)
|
|
||||||
{
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await _currentContext.EditSubscription(id))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var organizationUpdate = model.ToSecretsManagerSubscriptionUpdate(organization);
|
|
||||||
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(organizationUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/subscribe-secrets-manager")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task<ProfileOrganizationResponseModel> PostSubscribeSecretsManagerAsync(Guid id, [FromBody] SecretsManagerSubscribeRequestModel model)
|
|
||||||
{
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await _currentContext.EditSubscription(id))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
await _addSecretsManagerSubscriptionCommand.SignUpAsync(organization, model.AdditionalSmSeats,
|
|
||||||
model.AdditionalServiceAccounts);
|
|
||||||
|
|
||||||
var userId = _userService.GetProperUserId(User).Value;
|
|
||||||
|
|
||||||
await TryGrantOwnerAccessToSecretsManagerAsync(organization.Id, userId);
|
|
||||||
|
|
||||||
var organizationDetails = await _organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id,
|
|
||||||
OrganizationUserStatusType.Confirmed);
|
|
||||||
|
|
||||||
return new ProfileOrganizationResponseModel(organizationDetails);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/seat")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task<PaymentResponseModel> PostSeat(string id, [FromBody] OrganizationSeatRequestModel model)
|
|
||||||
{
|
|
||||||
var orgIdGuid = new Guid(id);
|
|
||||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _organizationService.AdjustSeatsAsync(orgIdGuid, model.SeatAdjustment.Value);
|
|
||||||
return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result };
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/storage")]
|
[HttpPost("{id}/storage")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<PaymentResponseModel> PostStorage(string id, [FromBody] StorageRequestModel model)
|
public async Task<PaymentResponseModel> PostStorage(string id, [FromBody] StorageRequestModel model)
|
||||||
@ -458,67 +217,6 @@ public class OrganizationsController : Controller
|
|||||||
return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result };
|
return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result };
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/verify-bank")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task PostVerifyBank(string id, [FromBody] OrganizationVerifyBankRequestModel model)
|
|
||||||
{
|
|
||||||
var orgIdGuid = new Guid(id);
|
|
||||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
await _organizationService.VerifyBankAsync(orgIdGuid, model.Amount1.Value, model.Amount2.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/cancel")]
|
|
||||||
public async Task PostCancel(Guid id, [FromBody] SubscriptionCancellationRequestModel request)
|
|
||||||
{
|
|
||||||
if (!await _currentContext.EditSubscription(id))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
|
||||||
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var subscription = await _subscriberQueries.GetSubscriptionOrThrow(organization);
|
|
||||||
|
|
||||||
await _cancelSubscriptionCommand.CancelSubscription(subscription,
|
|
||||||
new OffboardingSurveyResponse
|
|
||||||
{
|
|
||||||
UserId = _currentContext.UserId!.Value,
|
|
||||||
Reason = request.Reason,
|
|
||||||
Feedback = request.Feedback
|
|
||||||
},
|
|
||||||
organization.IsExpired());
|
|
||||||
|
|
||||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(
|
|
||||||
ReferenceEventType.CancelSubscription,
|
|
||||||
organization,
|
|
||||||
_currentContext)
|
|
||||||
{
|
|
||||||
EndOfPeriod = organization.IsExpired()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/reinstate")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task PostReinstate(string id)
|
|
||||||
{
|
|
||||||
var orgIdGuid = new Guid(id);
|
|
||||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
await _organizationService.ReinstateSubscriptionAsync(orgIdGuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/leave")]
|
[HttpPost("{id}/leave")]
|
||||||
public async Task Leave(string id)
|
public async Task Leave(string id)
|
||||||
{
|
{
|
||||||
@ -586,6 +284,37 @@ public class OrganizationsController : Controller
|
|||||||
await _organizationService.DeleteAsync(organization);
|
await _organizationService.DeleteAsync(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id}/delete-recover-token")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task PostDeleteRecoverToken(Guid id, [FromBody] OrganizationVerifyDeleteRecoverRequestModel model)
|
||||||
|
{
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_orgDeleteTokenDataFactory.TryUnprotect(model.Token, out var data) || !data.IsValid(organization))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Invalid token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||||
|
if (consolidatedBillingEnabled && organization.IsValidClient())
|
||||||
|
{
|
||||||
|
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||||
|
if (provider.IsBillable())
|
||||||
|
{
|
||||||
|
await _scaleSeatsCommand.ScalePasswordManagerSeats(
|
||||||
|
provider,
|
||||||
|
organization.PlanType,
|
||||||
|
-organization.Seats ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _organizationService.DeleteAsync(organization);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/import")]
|
[HttpPost("{id}/import")]
|
||||||
public async Task Import(string id, [FromBody] ImportOrganizationUsersRequestModel model)
|
public async Task Import(string id, [FromBody] ImportOrganizationUsersRequestModel model)
|
||||||
{
|
{
|
||||||
@ -722,55 +451,6 @@ public class OrganizationsController : Controller
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/tax")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task<TaxInfoResponseModel> GetTaxInfo(string id)
|
|
||||||
{
|
|
||||||
var orgIdGuid = new Guid(id);
|
|
||||||
if (!await _currentContext.OrganizationOwner(orgIdGuid))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var taxInfo = await _paymentService.GetTaxInfoAsync(organization);
|
|
||||||
return new TaxInfoResponseModel(taxInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("{id}/tax")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task PutTaxInfo(string id, [FromBody] ExpandedTaxInfoUpdateRequestModel model)
|
|
||||||
{
|
|
||||||
var orgIdGuid = new Guid(id);
|
|
||||||
if (!await _currentContext.OrganizationOwner(orgIdGuid))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var taxInfo = new TaxInfo
|
|
||||||
{
|
|
||||||
TaxIdNumber = model.TaxId,
|
|
||||||
BillingAddressLine1 = model.Line1,
|
|
||||||
BillingAddressLine2 = model.Line2,
|
|
||||||
BillingAddressCity = model.City,
|
|
||||||
BillingAddressState = model.State,
|
|
||||||
BillingAddressPostalCode = model.PostalCode,
|
|
||||||
BillingAddressCountry = model.Country,
|
|
||||||
};
|
|
||||||
await _paymentService.SaveTaxInfoAsync(organization, taxInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}/public-key")]
|
[HttpGet("{id}/public-key")]
|
||||||
public async Task<OrganizationPublicKeyResponseModel> GetPublicKey(string id)
|
public async Task<OrganizationPublicKeyResponseModel> GetPublicKey(string id)
|
||||||
{
|
{
|
||||||
@ -912,15 +592,4 @@ public class OrganizationsController : Controller
|
|||||||
ou.Type is OrganizationUserType.Admin or OrganizationUserType.Owner)
|
ou.Type is OrganizationUserType.Admin or OrganizationUserType.Owner)
|
||||||
.Select(ou => _pushNotificationService.PushSyncOrganizationsAsync(ou.UserId.Value)));
|
.Select(ou => _pushNotificationService.PushSyncOrganizationsAsync(ou.UserId.Value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TryGrantOwnerAccessToSecretsManagerAsync(Guid organizationId, Guid userId)
|
|
||||||
{
|
|
||||||
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
|
|
||||||
|
|
||||||
if (organizationUser != null)
|
|
||||||
{
|
|
||||||
organizationUser.AccessSecretsManager = true;
|
|
||||||
await _organizationUserRepository.ReplaceAsync(organizationUser);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ 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.Commands;
|
using Bit.Core.Billing.Commands;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -112,6 +113,9 @@ public class ProviderOrganizationsController : Controller
|
|||||||
providerOrganization,
|
providerOrganization,
|
||||||
organization);
|
organization);
|
||||||
|
|
||||||
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
|
if (organization.IsStripeEnabled())
|
||||||
|
{
|
||||||
|
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
|
|
||||||
|
public class OrganizationVerifyDeleteRecoverRequestModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Token { get; set; }
|
||||||
|
}
|
@ -34,7 +34,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.0" />
|
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.0" />
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.0" />
|
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.0" />
|
||||||
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.10.0" />
|
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.24.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
@ -43,7 +43,6 @@ using Bit.Core.Utilities;
|
|||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Bit.Core.Vault.Repositories;
|
using Bit.Core.Vault.Repositories;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Bit.Api.Auth.Controllers;
|
namespace Bit.Api.Auth.Controllers;
|
||||||
@ -438,59 +437,19 @@ public class AccountsController : Controller
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
IdentityResult result;
|
var dataModel = new RotateUserKeyData
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.KeyRotationImprovements))
|
|
||||||
{
|
{
|
||||||
var dataModel = new RotateUserKeyData
|
MasterPasswordHash = model.MasterPasswordHash,
|
||||||
{
|
Key = model.Key,
|
||||||
MasterPasswordHash = model.MasterPasswordHash,
|
PrivateKey = model.PrivateKey,
|
||||||
Key = model.Key,
|
Ciphers = await _cipherValidator.ValidateAsync(user, model.Ciphers),
|
||||||
PrivateKey = model.PrivateKey,
|
Folders = await _folderValidator.ValidateAsync(user, model.Folders),
|
||||||
Ciphers = await _cipherValidator.ValidateAsync(user, model.Ciphers),
|
Sends = await _sendValidator.ValidateAsync(user, model.Sends),
|
||||||
Folders = await _folderValidator.ValidateAsync(user, model.Folders),
|
EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.EmergencyAccessKeys),
|
||||||
Sends = await _sendValidator.ValidateAsync(user, model.Sends),
|
OrganizationUsers = await _organizationUserValidator.ValidateAsync(user, model.ResetPasswordKeys)
|
||||||
EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.EmergencyAccessKeys),
|
};
|
||||||
OrganizationUsers = await _organizationUserValidator.ValidateAsync(user, model.ResetPasswordKeys)
|
|
||||||
};
|
|
||||||
|
|
||||||
result = await _rotateUserKeyCommand.RotateUserKeyAsync(user, dataModel);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var ciphers = new List<Cipher>();
|
|
||||||
if (model.Ciphers.Any())
|
|
||||||
{
|
|
||||||
var existingCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, useFlexibleCollections: UseFlexibleCollections);
|
|
||||||
ciphers.AddRange(existingCiphers
|
|
||||||
.Join(model.Ciphers, c => c.Id, c => c.Id, (existing, c) => c.ToCipher(existing)));
|
|
||||||
}
|
|
||||||
|
|
||||||
var folders = new List<Folder>();
|
|
||||||
if (model.Folders.Any())
|
|
||||||
{
|
|
||||||
var existingFolders = await _folderRepository.GetManyByUserIdAsync(user.Id);
|
|
||||||
folders.AddRange(existingFolders
|
|
||||||
.Join(model.Folders, f => f.Id, f => f.Id, (existing, f) => f.ToFolder(existing)));
|
|
||||||
}
|
|
||||||
|
|
||||||
var sends = new List<Send>();
|
|
||||||
if (model.Sends?.Any() == true)
|
|
||||||
{
|
|
||||||
var existingSends = await _sendRepository.GetManyByUserIdAsync(user.Id);
|
|
||||||
sends.AddRange(existingSends
|
|
||||||
.Join(model.Sends, s => s.Id, s => s.Id, (existing, s) => s.ToSend(existing, _sendService)));
|
|
||||||
}
|
|
||||||
|
|
||||||
result = await _userService.UpdateKeyAsync(
|
|
||||||
user,
|
|
||||||
model.MasterPasswordHash,
|
|
||||||
model.Key,
|
|
||||||
model.PrivateKey,
|
|
||||||
ciphers,
|
|
||||||
folders,
|
|
||||||
sends);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
var result = await _rotateUserKeyCommand.RotateUserKeyAsync(user, dataModel);
|
||||||
|
|
||||||
if (result.Succeeded)
|
if (result.Succeeded)
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
using Bit.Api.Billing.Models.Responses;
|
using Bit.Api.Billing.Models.Responses;
|
||||||
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Core.Billing.Queries;
|
using Bit.Core.Billing.Queries;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -8,7 +14,10 @@ namespace Bit.Api.Billing.Controllers;
|
|||||||
[Route("organizations/{organizationId:guid}/billing")]
|
[Route("organizations/{organizationId:guid}/billing")]
|
||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
public class OrganizationBillingController(
|
public class OrganizationBillingController(
|
||||||
IOrganizationBillingQueries organizationBillingQueries) : Controller
|
IOrganizationBillingQueries organizationBillingQueries,
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IPaymentService paymentService) : Controller
|
||||||
{
|
{
|
||||||
[HttpGet("metadata")]
|
[HttpGet("metadata")]
|
||||||
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
|
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
|
||||||
@ -24,4 +33,23 @@ public class OrganizationBillingController(
|
|||||||
|
|
||||||
return TypedResults.Ok(response);
|
return TypedResults.Ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task<BillingResponseModel> GetBilling(Guid organizationId)
|
||||||
|
{
|
||||||
|
if (!await currentContext.ViewBillingHistory(organizationId))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var billingInfo = await paymentService.GetBillingAsync(organization);
|
||||||
|
return new BillingResponseModel(billingInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
385
src/Api/Billing/Controllers/OrganizationsController.cs
Normal file
385
src/Api/Billing/Controllers/OrganizationsController.cs
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
|
using Bit.Api.AdminConsole.Models.Response;
|
||||||
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
|
using Bit.Api.Models.Request;
|
||||||
|
using Bit.Api.Models.Request.Organizations;
|
||||||
|
using Bit.Api.Models.Response;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Billing.Commands;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Models;
|
||||||
|
using Bit.Core.Billing.Queries;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||||
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Tools.Enums;
|
||||||
|
using Bit.Core.Tools.Models.Business;
|
||||||
|
using Bit.Core.Tools.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bit.Api.Billing.Controllers;
|
||||||
|
|
||||||
|
[Route("organizations")]
|
||||||
|
[Authorize("Application")]
|
||||||
|
public class OrganizationsController(
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationService organizationService,
|
||||||
|
IUserService userService,
|
||||||
|
IPaymentService paymentService,
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
|
||||||
|
GlobalSettings globalSettings,
|
||||||
|
ILicensingService licensingService,
|
||||||
|
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||||
|
IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand,
|
||||||
|
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
|
||||||
|
ICancelSubscriptionCommand cancelSubscriptionCommand,
|
||||||
|
ISubscriberQueries subscriberQueries,
|
||||||
|
IReferenceEventService referenceEventService)
|
||||||
|
: Controller
|
||||||
|
{
|
||||||
|
[HttpGet("{id}/billing-status")]
|
||||||
|
public async Task<OrganizationBillingStatusResponseModel> GetBillingStatus(Guid id)
|
||||||
|
{
|
||||||
|
if (!await currentContext.EditPaymentMethods(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(id);
|
||||||
|
|
||||||
|
var risksSubscriptionFailure = await paymentService.RisksSubscriptionFailure(organization);
|
||||||
|
|
||||||
|
return new OrganizationBillingStatusResponseModel(organization, risksSubscriptionFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id:guid}/subscription")]
|
||||||
|
public async Task<OrganizationSubscriptionResponseModel> GetSubscription(Guid id)
|
||||||
|
{
|
||||||
|
if (!await currentContext.ViewSubscription(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(id);
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalSettings.SelfHosted && organization.Gateway != null)
|
||||||
|
{
|
||||||
|
var subscriptionInfo = await paymentService.GetSubscriptionAsync(organization);
|
||||||
|
if (subscriptionInfo == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var hideSensitiveData = !await currentContext.EditSubscription(id);
|
||||||
|
|
||||||
|
return new OrganizationSubscriptionResponseModel(organization, subscriptionInfo, hideSensitiveData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (globalSettings.SelfHosted)
|
||||||
|
{
|
||||||
|
var orgLicense = await licensingService.ReadOrganizationLicenseAsync(organization);
|
||||||
|
return new OrganizationSubscriptionResponseModel(organization, orgLicense);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OrganizationSubscriptionResponseModel(organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id:guid}/license")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task<OrganizationLicense> GetLicense(Guid id, [FromQuery] Guid installationId)
|
||||||
|
{
|
||||||
|
if (!await currentContext.OrganizationOwner(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var org = await organizationRepository.GetByIdAsync(id);
|
||||||
|
var license = await cloudGetOrganizationLicenseQuery.GetLicenseAsync(org, installationId);
|
||||||
|
if (license == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return license;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/payment")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task PostPayment(Guid id, [FromBody] PaymentRequestModel model)
|
||||||
|
{
|
||||||
|
if (!await currentContext.EditPaymentMethods(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await organizationService.ReplacePaymentMethodAsync(id, model.PaymentToken,
|
||||||
|
model.PaymentMethodType.Value, new TaxInfo
|
||||||
|
{
|
||||||
|
BillingAddressLine1 = model.Line1,
|
||||||
|
BillingAddressLine2 = model.Line2,
|
||||||
|
BillingAddressState = model.State,
|
||||||
|
BillingAddressCity = model.City,
|
||||||
|
BillingAddressPostalCode = model.PostalCode,
|
||||||
|
BillingAddressCountry = model.Country,
|
||||||
|
TaxIdNumber = model.TaxId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/upgrade")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task<PaymentResponseModel> PostUpgrade(Guid id, [FromBody] OrganizationUpgradeRequestModel model)
|
||||||
|
{
|
||||||
|
if (!await currentContext.EditSubscription(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var (success, paymentIntentClientSecret) = await upgradeOrganizationPlanCommand.UpgradePlanAsync(id, model.ToOrganizationUpgrade());
|
||||||
|
|
||||||
|
if (model.UseSecretsManager && success)
|
||||||
|
{
|
||||||
|
var userId = userService.GetProperUserId(User).Value;
|
||||||
|
|
||||||
|
await TryGrantOwnerAccessToSecretsManagerAsync(id, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PaymentResponseModel { Success = success, PaymentIntentClientSecret = paymentIntentClientSecret };
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id}/sm-subscription")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task PostSmSubscription(Guid id, [FromBody] SecretsManagerSubscriptionUpdateRequestModel model)
|
||||||
|
{
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(id);
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await currentContext.EditSubscription(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
organization = await AdjustOrganizationSeatsForSmTrialAsync(id, organization, model);
|
||||||
|
|
||||||
|
var organizationUpdate = model.ToSecretsManagerSubscriptionUpdate(organization);
|
||||||
|
|
||||||
|
await updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(organizationUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/subscription")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task PostSubscription(Guid id, [FromBody] OrganizationSubscriptionUpdateRequestModel model)
|
||||||
|
{
|
||||||
|
if (!await currentContext.EditSubscription(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
await organizationService.UpdateSubscription(id, model.SeatAdjustment, model.MaxAutoscaleSeats);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/subscribe-secrets-manager")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task<ProfileOrganizationResponseModel> PostSubscribeSecretsManagerAsync(Guid id, [FromBody] SecretsManagerSubscribeRequestModel model)
|
||||||
|
{
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(id);
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await currentContext.EditSubscription(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await addSecretsManagerSubscriptionCommand.SignUpAsync(organization, model.AdditionalSmSeats,
|
||||||
|
model.AdditionalServiceAccounts);
|
||||||
|
|
||||||
|
var userId = userService.GetProperUserId(User).Value;
|
||||||
|
|
||||||
|
await TryGrantOwnerAccessToSecretsManagerAsync(organization.Id, userId);
|
||||||
|
|
||||||
|
var organizationDetails = await organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id,
|
||||||
|
OrganizationUserStatusType.Confirmed);
|
||||||
|
|
||||||
|
return new ProfileOrganizationResponseModel(organizationDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/seat")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task<PaymentResponseModel> PostSeat(Guid id, [FromBody] OrganizationSeatRequestModel model)
|
||||||
|
{
|
||||||
|
if (!await currentContext.EditSubscription(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await organizationService.AdjustSeatsAsync(id, model.SeatAdjustment.Value);
|
||||||
|
return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result };
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/verify-bank")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task PostVerifyBank(Guid id, [FromBody] OrganizationVerifyBankRequestModel model)
|
||||||
|
{
|
||||||
|
if (!await currentContext.EditSubscription(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await organizationService.VerifyBankAsync(id, model.Amount1.Value, model.Amount2.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id}/cancel")]
|
||||||
|
public async Task PostCancel(Guid id, [FromBody] SubscriptionCancellationRequestModel request)
|
||||||
|
{
|
||||||
|
if (!await currentContext.EditSubscription(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(id);
|
||||||
|
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscription = await subscriberQueries.GetSubscriptionOrThrow(organization);
|
||||||
|
|
||||||
|
await cancelSubscriptionCommand.CancelSubscription(subscription,
|
||||||
|
new OffboardingSurveyResponse
|
||||||
|
{
|
||||||
|
UserId = currentContext.UserId!.Value,
|
||||||
|
Reason = request.Reason,
|
||||||
|
Feedback = request.Feedback
|
||||||
|
},
|
||||||
|
organization.IsExpired());
|
||||||
|
|
||||||
|
await referenceEventService.RaiseEventAsync(new ReferenceEvent(
|
||||||
|
ReferenceEventType.CancelSubscription,
|
||||||
|
organization,
|
||||||
|
currentContext)
|
||||||
|
{
|
||||||
|
EndOfPeriod = organization.IsExpired()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/reinstate")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task PostReinstate(Guid id)
|
||||||
|
{
|
||||||
|
if (!await currentContext.EditSubscription(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await organizationService.ReinstateSubscriptionAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id:guid}/tax")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task<TaxInfoResponseModel> GetTaxInfo(Guid id)
|
||||||
|
{
|
||||||
|
if (!await currentContext.OrganizationOwner(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(id);
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var taxInfo = await paymentService.GetTaxInfoAsync(organization);
|
||||||
|
return new TaxInfoResponseModel(taxInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id:guid}/tax")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task PutTaxInfo(Guid id, [FromBody] ExpandedTaxInfoUpdateRequestModel model)
|
||||||
|
{
|
||||||
|
if (!await currentContext.OrganizationOwner(id))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(id);
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var taxInfo = new TaxInfo
|
||||||
|
{
|
||||||
|
TaxIdNumber = model.TaxId,
|
||||||
|
BillingAddressLine1 = model.Line1,
|
||||||
|
BillingAddressLine2 = model.Line2,
|
||||||
|
BillingAddressCity = model.City,
|
||||||
|
BillingAddressState = model.State,
|
||||||
|
BillingAddressPostalCode = model.PostalCode,
|
||||||
|
BillingAddressCountry = model.Country,
|
||||||
|
};
|
||||||
|
await paymentService.SaveTaxInfoAsync(organization, taxInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to grant owner access to the Secrets Manager for the organization
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationId"></param>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
private async Task TryGrantOwnerAccessToSecretsManagerAsync(Guid organizationId, Guid userId)
|
||||||
|
{
|
||||||
|
var organizationUser = await organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
|
||||||
|
|
||||||
|
if (organizationUser != null)
|
||||||
|
{
|
||||||
|
organizationUser.AccessSecretsManager = true;
|
||||||
|
await organizationUserRepository.ReplaceAsync(organizationUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adjusts the organization seats for the Secrets Manager trial to match the new seat count for secrets manager
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id"></param>
|
||||||
|
/// <param name="organization"></param>
|
||||||
|
/// <param name="model"></param>
|
||||||
|
private async Task<Organization> AdjustOrganizationSeatsForSmTrialAsync(Guid id, Organization organization,
|
||||||
|
SecretsManagerSubscriptionUpdateRequestModel model)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId) ||
|
||||||
|
string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId) ||
|
||||||
|
model.SeatAdjustment == 0)
|
||||||
|
{
|
||||||
|
return organization;
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscriptionInfo = await paymentService.GetSubscriptionAsync(organization);
|
||||||
|
if (subscriptionInfo?.CustomerDiscount?.Id != StripeConstants.CouponIDs.SecretsManagerStandalone)
|
||||||
|
{
|
||||||
|
return organization;
|
||||||
|
}
|
||||||
|
|
||||||
|
await organizationService.UpdateSubscription(id, model.SeatAdjustment, null);
|
||||||
|
|
||||||
|
return await organizationRepository.GetByIdAsync(id);
|
||||||
|
}
|
||||||
|
}
|
@ -133,10 +133,17 @@ public class ProviderClientsController(
|
|||||||
return TypedResults.Problem();
|
return TypedResults.Problem();
|
||||||
}
|
}
|
||||||
|
|
||||||
await assignSeatsToClientOrganizationCommand.AssignSeatsToClientOrganization(
|
if (clientOrganization.Seats != requestBody.AssignedSeats)
|
||||||
provider,
|
{
|
||||||
clientOrganization,
|
await assignSeatsToClientOrganizationCommand.AssignSeatsToClientOrganization(
|
||||||
requestBody.AssignedSeats);
|
provider,
|
||||||
|
clientOrganization,
|
||||||
|
requestBody.AssignedSeats);
|
||||||
|
}
|
||||||
|
|
||||||
|
clientOrganization.Name = requestBody.Name;
|
||||||
|
|
||||||
|
await organizationRepository.ReplaceAsync(clientOrganization);
|
||||||
|
|
||||||
return TypedResults.Ok();
|
return TypedResults.Ok();
|
||||||
}
|
}
|
||||||
|
@ -7,4 +7,7 @@ public class UpdateClientOrganizationRequestBody
|
|||||||
[Required]
|
[Required]
|
||||||
[Range(0, int.MaxValue, ErrorMessage = "You cannot assign negative seats to a client organization.")]
|
[Range(0, int.MaxValue, ErrorMessage = "You cannot assign negative seats to a client organization.")]
|
||||||
public int AssignedSeats { get; set; }
|
public int AssignedSeats { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Name { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -89,6 +89,7 @@ public class CollectionAccessDetailsResponseModel : CollectionResponseModel
|
|||||||
ReadOnly = collection.ReadOnly;
|
ReadOnly = collection.ReadOnly;
|
||||||
HidePasswords = collection.HidePasswords;
|
HidePasswords = collection.HidePasswords;
|
||||||
Manage = collection.Manage;
|
Manage = collection.Manage;
|
||||||
|
Unmanaged = collection.Unmanaged;
|
||||||
Groups = collection.Groups?.Select(g => new SelectionReadOnlyResponseModel(g)) ?? Enumerable.Empty<SelectionReadOnlyResponseModel>();
|
Groups = collection.Groups?.Select(g => new SelectionReadOnlyResponseModel(g)) ?? Enumerable.Empty<SelectionReadOnlyResponseModel>();
|
||||||
Users = collection.Users?.Select(g => new SelectionReadOnlyResponseModel(g)) ?? Enumerable.Empty<SelectionReadOnlyResponseModel>();
|
Users = collection.Users?.Select(g => new SelectionReadOnlyResponseModel(g)) ?? Enumerable.Empty<SelectionReadOnlyResponseModel>();
|
||||||
}
|
}
|
||||||
@ -104,4 +105,5 @@ public class CollectionAccessDetailsResponseModel : CollectionResponseModel
|
|||||||
public bool ReadOnly { get; set; }
|
public bool ReadOnly { get; set; }
|
||||||
public bool HidePasswords { get; set; }
|
public bool HidePasswords { get; set; }
|
||||||
public bool Manage { get; set; }
|
public bool Manage { get; set; }
|
||||||
|
public bool Unmanaged { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -217,34 +217,49 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
|||||||
|
|
||||||
private async Task<bool> CanUpdateUserAccessAsync(ICollection<Collection> resources, CurrentContextOrganization? org)
|
private async Task<bool> CanUpdateUserAccessAsync(ICollection<Collection> resources, CurrentContextOrganization? org)
|
||||||
{
|
{
|
||||||
return await CanUpdateCollectionAsync(resources, org) || org?.Permissions.ManageUsers == true;
|
if (await AllowAdminAccessToAllCollectionItems(org) && org?.Permissions.ManageUsers == true)
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> CanUpdateGroupAccessAsync(ICollection<Collection> resources, CurrentContextOrganization? org)
|
|
||||||
{
|
|
||||||
return await CanUpdateCollectionAsync(resources, org) || org?.Permissions.ManageGroups == true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> CanDeleteAsync(ICollection<Collection> resources, CurrentContextOrganization? org)
|
|
||||||
{
|
|
||||||
// Owners, Admins, and users with DeleteAnyCollection permission can always delete collections
|
|
||||||
if (org is
|
|
||||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
|
||||||
{ Permissions.DeleteAnyCollection: true })
|
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for non-null org here: the user must be apart of the organization for this setting to take affect
|
return await CanUpdateCollectionAsync(resources, org);
|
||||||
// The limit collection management setting is disabled,
|
}
|
||||||
// ensure acting user has manage permissions for all collections being deleted
|
|
||||||
if (await GetOrganizationAbilityAsync(org) is { LimitCollectionCreationDeletion: false })
|
private async Task<bool> CanUpdateGroupAccessAsync(ICollection<Collection> resources, CurrentContextOrganization? org)
|
||||||
|
{
|
||||||
|
if (await AllowAdminAccessToAllCollectionItems(org) && org?.Permissions.ManageGroups == true)
|
||||||
{
|
{
|
||||||
var canManageCollections = await CanManageCollectionsAsync(resources, org);
|
return true;
|
||||||
if (canManageCollections)
|
}
|
||||||
{
|
|
||||||
return true;
|
return await CanUpdateCollectionAsync(resources, org);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> CanDeleteAsync(ICollection<Collection> resources, CurrentContextOrganization? org)
|
||||||
|
{
|
||||||
|
// Users with DeleteAnyCollection permission can always delete collections
|
||||||
|
if (org is { Permissions.DeleteAnyCollection: true })
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If AllowAdminAccessToAllCollectionItems is true, Owners and Admins can delete any collection, regardless of LimitCollectionCreationDeletion setting
|
||||||
|
var organizationAbility = await GetOrganizationAbilityAsync(org);
|
||||||
|
var allowAdminAccessToAllCollectionItems = !_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1) ||
|
||||||
|
organizationAbility is { AllowAdminAccessToAllCollectionItems: true };
|
||||||
|
if (allowAdminAccessToAllCollectionItems && org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin })
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If LimitCollectionCreationDeletion is false, AllowAdminAccessToAllCollectionItems setting is irrelevant.
|
||||||
|
// Ensure acting user has manage permissions for all collections being deleted
|
||||||
|
// If LimitCollectionCreationDeletion is true, only Owners and Admins can delete collections they manage
|
||||||
|
var canDeleteManagedCollections = organizationAbility is { LimitCollectionCreationDeletion: false } ||
|
||||||
|
org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
|
||||||
|
if (canDeleteManagedCollections && await CanManageCollectionsAsync(resources, org))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow providers to delete collections if they are a provider for the target organization
|
// Allow providers to delete collections if they are a provider for the target organization
|
||||||
@ -308,4 +323,11 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
|||||||
|
|
||||||
return await _applicationCacheService.GetOrganizationAbilityAsync(organization.Id);
|
return await _applicationCacheService.GetOrganizationAbilityAsync(organization.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> AllowAdminAccessToAllCollectionItems(CurrentContextOrganization? org)
|
||||||
|
{
|
||||||
|
var organizationAbility = await GetOrganizationAbilityAsync(org);
|
||||||
|
return !_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1) ||
|
||||||
|
organizationAbility is { AllowAdminAccessToAllCollectionItems: true };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -619,9 +619,39 @@ public class CiphersController : Controller
|
|||||||
|
|
||||||
var updatedCipher = await GetByIdAsync(id, userId);
|
var updatedCipher = await GetByIdAsync(id, userId);
|
||||||
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id, UseFlexibleCollections);
|
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id, UseFlexibleCollections);
|
||||||
|
|
||||||
return new CipherDetailsResponseModel(updatedCipher, _globalSettings, collectionCiphers);
|
return new CipherDetailsResponseModel(updatedCipher, _globalSettings, collectionCiphers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id}/collections_v2")]
|
||||||
|
[HttpPost("{id}/collections_v2")]
|
||||||
|
public async Task<OptionalCipherDetailsResponseModel> PutCollections_vNext(Guid id, [FromBody] CipherCollectionsRequestModel model)
|
||||||
|
{
|
||||||
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
|
var cipher = await GetByIdAsync(id, userId);
|
||||||
|
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
||||||
|
!await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _cipherService.SaveCollectionsAsync(cipher,
|
||||||
|
model.CollectionIds.Select(c => new Guid(c)), userId, false);
|
||||||
|
|
||||||
|
var updatedCipher = await GetByIdAsync(id, userId);
|
||||||
|
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id, UseFlexibleCollections);
|
||||||
|
// If a user removes the last Can Manage access of a cipher, the "updatedCipher" will return null
|
||||||
|
// We will be returning an "Unavailable" property so the client knows the user can no longer access this
|
||||||
|
var response = new OptionalCipherDetailsResponseModel()
|
||||||
|
{
|
||||||
|
Unavailable = updatedCipher is null,
|
||||||
|
Cipher = updatedCipher is null
|
||||||
|
? null
|
||||||
|
: new CipherDetailsResponseModel(updatedCipher, _globalSettings, collectionCiphers)
|
||||||
|
};
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPut("{id}/collections-admin")]
|
[HttpPut("{id}/collections-admin")]
|
||||||
[HttpPost("{id}/collections-admin")]
|
[HttpPost("{id}/collections-admin")]
|
||||||
public async Task PutCollectionsAdmin(string id, [FromBody] CipherCollectionsRequestModel model)
|
public async Task PutCollectionsAdmin(string id, [FromBody] CipherCollectionsRequestModel model)
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
using Bit.Api.Vault.Models.Response;
|
||||||
|
using Bit.Core.Models.Api;
|
||||||
|
|
||||||
|
public class OptionalCipherDetailsResponseModel : ResponseModel
|
||||||
|
{
|
||||||
|
public bool Unavailable { get; set; }
|
||||||
|
|
||||||
|
public CipherDetailsResponseModel? Cipher { get; set; }
|
||||||
|
|
||||||
|
public OptionalCipherDetailsResponseModel()
|
||||||
|
: base("optionalCipherDetails")
|
||||||
|
{ }
|
||||||
|
}
|
@ -3,6 +3,7 @@ using Bit.Billing.Models;
|
|||||||
using Bit.Billing.Services;
|
using Bit.Billing.Services;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -55,6 +56,7 @@ public class StripeController : Controller
|
|||||||
private readonly IStripeEventService _stripeEventService;
|
private readonly IStripeEventService _stripeEventService;
|
||||||
private readonly IStripeFacade _stripeFacade;
|
private readonly IStripeFacade _stripeFacade;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly IProviderRepository _providerRepository;
|
||||||
|
|
||||||
public StripeController(
|
public StripeController(
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
@ -74,7 +76,8 @@ public class StripeController : Controller
|
|||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IStripeEventService stripeEventService,
|
IStripeEventService stripeEventService,
|
||||||
IStripeFacade stripeFacade,
|
IStripeFacade stripeFacade,
|
||||||
IFeatureService featureService)
|
IFeatureService featureService,
|
||||||
|
IProviderRepository providerRepository)
|
||||||
{
|
{
|
||||||
_billingSettings = billingSettings?.Value;
|
_billingSettings = billingSettings?.Value;
|
||||||
_hostingEnvironment = hostingEnvironment;
|
_hostingEnvironment = hostingEnvironment;
|
||||||
@ -102,6 +105,7 @@ public class StripeController : Controller
|
|||||||
_stripeEventService = stripeEventService;
|
_stripeEventService = stripeEventService;
|
||||||
_stripeFacade = stripeFacade;
|
_stripeFacade = stripeFacade;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
|
_providerRepository = providerRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("webhook")]
|
[HttpPost("webhook")]
|
||||||
@ -425,7 +429,61 @@ public class StripeController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (organizationId, userId, providerId) = GetIdsFromMetadata(subscription.Metadata);
|
var (organizationId, userId, providerId) = GetIdsFromMetadata(subscription.Metadata);
|
||||||
if (organizationId.HasValue)
|
|
||||||
|
if (providerId.HasValue)
|
||||||
|
{
|
||||||
|
var provider = await _providerRepository.GetByIdAsync(providerId.Value);
|
||||||
|
|
||||||
|
if (provider == null)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Received invoice.payment_succeeded webhook ({EventID}) for Provider ({ProviderID}) that does not exist",
|
||||||
|
parsedEvent.Id,
|
||||||
|
providerId.Value);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var teamsMonthly = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||||
|
|
||||||
|
var enterpriseMonthly = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
|
||||||
|
|
||||||
|
var teamsMonthlyLineItem =
|
||||||
|
subscription.Items.Data.FirstOrDefault(item =>
|
||||||
|
item.Plan.Id == teamsMonthly.PasswordManager.StripeSeatPlanId);
|
||||||
|
|
||||||
|
var enterpriseMonthlyLineItem =
|
||||||
|
subscription.Items.Data.FirstOrDefault(item =>
|
||||||
|
item.Plan.Id == enterpriseMonthly.PasswordManager.StripeSeatPlanId);
|
||||||
|
|
||||||
|
if (teamsMonthlyLineItem == null || enterpriseMonthlyLineItem == null)
|
||||||
|
{
|
||||||
|
_logger.LogError("invoice.payment_succeeded webhook ({EventID}) for Provider ({ProviderID}) indicates missing subscription line items",
|
||||||
|
parsedEvent.Id,
|
||||||
|
provider.Id);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _referenceEventService.RaiseEventAsync(new ReferenceEvent
|
||||||
|
{
|
||||||
|
Type = ReferenceEventType.Rebilled,
|
||||||
|
Source = ReferenceEventSource.Provider,
|
||||||
|
Id = provider.Id,
|
||||||
|
PlanType = PlanType.TeamsMonthly,
|
||||||
|
Seats = (int)teamsMonthlyLineItem.Quantity
|
||||||
|
});
|
||||||
|
|
||||||
|
await _referenceEventService.RaiseEventAsync(new ReferenceEvent
|
||||||
|
{
|
||||||
|
Type = ReferenceEventType.Rebilled,
|
||||||
|
Source = ReferenceEventSource.Provider,
|
||||||
|
Id = provider.Id,
|
||||||
|
PlanType = PlanType.EnterpriseMonthly,
|
||||||
|
Seats = (int)enterpriseMonthlyLineItem.Quantity
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (organizationId.HasValue)
|
||||||
{
|
{
|
||||||
if (!subscription.Items.Any(i =>
|
if (!subscription.Items.Any(i =>
|
||||||
StaticStore.Plans.Any(p => p.PasswordManager.StripePlanId == i.Plan.Id)))
|
StaticStore.Plans.Any(p => p.PasswordManager.StripePlanId == i.Plan.Id)))
|
||||||
@ -657,6 +715,23 @@ public class StripeController : Controller
|
|||||||
await SendEmails(new List<string> { user.Email });
|
await SendEmails(new List<string> { user.Email });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (providerId.HasValue)
|
||||||
|
{
|
||||||
|
var provider = await _providerRepository.GetByIdAsync(providerId.Value);
|
||||||
|
|
||||||
|
if (provider == null)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Received invoice.Upcoming webhook ({EventID}) for Provider ({ProviderID}) that does not exist",
|
||||||
|
parsedEvent.Id,
|
||||||
|
providerId.Value);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendEmails(new List<string> { provider.BillingEmail });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -86,20 +86,20 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
|||||||
public int? MaxAutoscaleSmSeats { get; set; }
|
public int? MaxAutoscaleSmSeats { get; set; }
|
||||||
public int? MaxAutoscaleSmServiceAccounts { get; set; }
|
public int? MaxAutoscaleSmServiceAccounts { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Refers to the ability for an organization to limit collection creation and deletion to owners and admins only
|
/// If set to true, only owners, admins, and some custom users can create and delete collections.
|
||||||
|
/// If set to false, any organization member can create a collection, and any member can delete a collection that
|
||||||
|
/// they have Can Manage permissions for.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool LimitCollectionCreationDeletion { get; set; }
|
public bool LimitCollectionCreationDeletion { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Refers to the ability for an organization to limit owner/admin access to all collection items
|
/// If set to true, admins, owners, and some custom users can read/write all collections and items in the Admin Console.
|
||||||
/// <remarks>
|
/// If set to false, users generally need collection-level permissions to read/write a collection or its items.
|
||||||
/// True: Owner/admins can access all items belonging to any collections
|
|
||||||
/// False: Owner/admins can only access items for collections they are assigned
|
|
||||||
/// </remarks>
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// True if the organization is using the Flexible Collections permission changes, false otherwise.
|
/// This is an organization-level feature flag (not controlled via LaunchDarkly) to onboard organizations to the
|
||||||
/// For existing organizations, this must only be set to true once data migrations have been run for this organization.
|
/// Flexible Collections MVP changes. This has been fully released and must always be set to TRUE for all organizations.
|
||||||
|
/// AC-1714 will remove this flag after all old code has been removed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool FlexibleCollections { get; set; }
|
public bool FlexibleCollections { get; set; }
|
||||||
|
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||||
|
|
||||||
|
public class OrgDeleteTokenable : Tokens.ExpiringTokenable
|
||||||
|
{
|
||||||
|
public const string ClearTextPrefix = "";
|
||||||
|
public const string DataProtectorPurpose = "OrgDeleteDataProtector";
|
||||||
|
public const string TokenIdentifier = "OrgDelete";
|
||||||
|
public string Identifier { get; set; } = TokenIdentifier;
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public OrgDeleteTokenable(DateTime expirationDate)
|
||||||
|
{
|
||||||
|
ExpirationDate = expirationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OrgDeleteTokenable(Organization organization, int hoursTillExpiration)
|
||||||
|
{
|
||||||
|
Id = organization.Id;
|
||||||
|
ExpirationDate = DateTime.UtcNow.AddHours(hoursTillExpiration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsValid(Organization organization)
|
||||||
|
{
|
||||||
|
return Id == organization.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool TokenIsValid() => Identifier == TokenIdentifier && Id != default;
|
||||||
|
}
|
@ -34,6 +34,7 @@ public interface IOrganizationService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(OrganizationLicense license, User owner,
|
Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(OrganizationLicense license, User owner,
|
||||||
string ownerKey, string collectionName, string publicKey, string privateKey);
|
string ownerKey, string collectionName, string publicKey, string privateKey);
|
||||||
|
Task InitiateDeleteAsync(Organization organization, string orgAdminEmail);
|
||||||
Task DeleteAsync(Organization organization);
|
Task DeleteAsync(Organization organization);
|
||||||
Task EnableAsync(Guid organizationId, DateTime? expirationDate);
|
Task EnableAsync(Guid organizationId, DateTime? expirationDate);
|
||||||
Task DisableAsync(Guid organizationId, DateTime? expirationDate);
|
Task DisableAsync(Guid organizationId, DateTime? expirationDate);
|
||||||
@ -49,7 +50,7 @@ public interface IOrganizationService
|
|||||||
Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
|
Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
|
||||||
OrganizationUserType type, bool accessAll, string externalId, ICollection<CollectionAccessSelection> collections, IEnumerable<Guid> groups);
|
OrganizationUserType type, bool accessAll, string externalId, ICollection<CollectionAccessSelection> collections, IEnumerable<Guid> groups);
|
||||||
Task<OrganizationUser> InviteUserAsync(Guid organizationId, EventSystemUser systemUser, string email,
|
Task<OrganizationUser> InviteUserAsync(Guid organizationId, EventSystemUser systemUser, string email,
|
||||||
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups);
|
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups, bool accessSecretsManager);
|
||||||
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
|
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
|
||||||
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false);
|
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false);
|
||||||
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
||||||
|
@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities;
|
|||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Business;
|
using Bit.Core.AdminConsole.Models.Business;
|
||||||
|
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
@ -60,6 +61,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
private readonly IProviderUserRepository _providerUserRepository;
|
private readonly IProviderUserRepository _providerUserRepository;
|
||||||
private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;
|
private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;
|
||||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||||
|
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
|
||||||
private readonly IProviderRepository _providerRepository;
|
private readonly IProviderRepository _providerRepository;
|
||||||
private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory;
|
private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory;
|
||||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||||
@ -94,6 +96,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory,
|
IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory,
|
||||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
||||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||||
|
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
IFeatureService featureService)
|
IFeatureService featureService)
|
||||||
{
|
{
|
||||||
@ -123,6 +126,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
_providerUserRepository = providerUserRepository;
|
_providerUserRepository = providerUserRepository;
|
||||||
_countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;
|
_countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;
|
||||||
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
||||||
|
_orgDeleteTokenDataFactory = orgDeleteTokenDataFactory;
|
||||||
_providerRepository = providerRepository;
|
_providerRepository = providerRepository;
|
||||||
_orgUserInviteTokenableFactory = orgUserInviteTokenableFactory;
|
_orgUserInviteTokenableFactory = orgUserInviteTokenableFactory;
|
||||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||||
@ -434,9 +438,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
ValidatePlan(plan, signup.AdditionalSeats, "Password Manager");
|
ValidatePlan(plan, signup.AdditionalSeats, "Password Manager");
|
||||||
|
|
||||||
var flexibleCollectionsSignupEnabled =
|
|
||||||
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsSignup);
|
|
||||||
|
|
||||||
var flexibleCollectionsV1Enabled =
|
var flexibleCollectionsV1Enabled =
|
||||||
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1);
|
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1);
|
||||||
|
|
||||||
@ -478,14 +479,12 @@ public class OrganizationService : IOrganizationService
|
|||||||
// Secrets Manager not available for purchase with Consolidated Billing.
|
// Secrets Manager not available for purchase with Consolidated Billing.
|
||||||
UseSecretsManager = false,
|
UseSecretsManager = false,
|
||||||
|
|
||||||
// This feature flag indicates that new organizations should be automatically onboarded to
|
// Flexible Collections MVP is fully released and all organizations must always have this setting enabled.
|
||||||
// Flexible Collections enhancements
|
// AC-1714 will remove this flag after all old code has been removed.
|
||||||
FlexibleCollections = flexibleCollectionsSignupEnabled,
|
FlexibleCollections = true,
|
||||||
|
|
||||||
// These collection management settings smooth the migration for existing organizations by disabling some FC behavior.
|
// This is a transitional setting that defaults to ON until Flexible Collections v1 is released
|
||||||
// If the organization is onboarded to Flexible Collections on signup, we turn them OFF to enable all new behaviour.
|
// (to preserve existing behavior) and defaults to OFF after release (enabling new behavior)
|
||||||
// If the organization is NOT onboarded now, they will have to be migrated later, so they default to ON to limit FC changes on migration.
|
|
||||||
LimitCollectionCreationDeletion = !flexibleCollectionsSignupEnabled,
|
|
||||||
AllowAdminAccessToAllCollectionItems = !flexibleCollectionsV1Enabled
|
AllowAdminAccessToAllCollectionItems = !flexibleCollectionsV1Enabled
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -529,9 +528,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
await ValidateSignUpPoliciesAsync(signup.Owner.Id);
|
await ValidateSignUpPoliciesAsync(signup.Owner.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
var flexibleCollectionsSignupEnabled =
|
|
||||||
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsSignup);
|
|
||||||
|
|
||||||
var flexibleCollectionsV1IsEnabled =
|
var flexibleCollectionsV1IsEnabled =
|
||||||
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1);
|
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1);
|
||||||
|
|
||||||
@ -573,14 +569,12 @@ public class OrganizationService : IOrganizationService
|
|||||||
UsePasswordManager = true,
|
UsePasswordManager = true,
|
||||||
UseSecretsManager = signup.UseSecretsManager,
|
UseSecretsManager = signup.UseSecretsManager,
|
||||||
|
|
||||||
// This feature flag indicates that new organizations should be automatically onboarded to
|
// Flexible Collections MVP is fully released and all organizations must always have this setting enabled.
|
||||||
// Flexible Collections enhancements
|
// AC-1714 will remove this flag after all old code has been removed.
|
||||||
FlexibleCollections = flexibleCollectionsSignupEnabled,
|
FlexibleCollections = true,
|
||||||
|
|
||||||
// These collection management settings smooth the migration for existing organizations by disabling some FC behavior.
|
// This is a transitional setting that defaults to ON until Flexible Collections v1 is released
|
||||||
// If the organization is onboarded to Flexible Collections on signup, we turn them OFF to enable all new behaviour.
|
// (to preserve existing behavior) and defaults to OFF after release (enabling new behavior)
|
||||||
// If the organization is NOT onboarded now, they will have to be migrated later, so they default to ON to limit FC changes on migration.
|
|
||||||
LimitCollectionCreationDeletion = !flexibleCollectionsSignupEnabled,
|
|
||||||
AllowAdminAccessToAllCollectionItems = !flexibleCollectionsV1IsEnabled
|
AllowAdminAccessToAllCollectionItems = !flexibleCollectionsV1IsEnabled
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -661,9 +655,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
await ValidateSignUpPoliciesAsync(owner.Id);
|
await ValidateSignUpPoliciesAsync(owner.Id);
|
||||||
|
|
||||||
var flexibleCollectionsSignupEnabled =
|
|
||||||
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsSignup);
|
|
||||||
|
|
||||||
var organization = new Organization
|
var organization = new Organization
|
||||||
{
|
{
|
||||||
Name = license.Name,
|
Name = license.Name,
|
||||||
@ -709,7 +700,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
// This feature flag indicates that new organizations should be automatically onboarded to
|
// This feature flag indicates that new organizations should be automatically onboarded to
|
||||||
// Flexible Collections enhancements
|
// Flexible Collections enhancements
|
||||||
FlexibleCollections = flexibleCollectionsSignupEnabled,
|
FlexibleCollections = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
|
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
|
||||||
@ -811,6 +802,23 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task InitiateDeleteAsync(Organization organization, string orgAdminEmail)
|
||||||
|
{
|
||||||
|
var orgAdmin = await _userRepository.GetByEmailAsync(orgAdminEmail);
|
||||||
|
if (orgAdmin == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Org admin not found.");
|
||||||
|
}
|
||||||
|
var orgAdminOrgUser = await _organizationUserRepository.GetDetailsByUserAsync(orgAdmin.Id, organization.Id);
|
||||||
|
if (orgAdminOrgUser == null || orgAdminOrgUser.Status != OrganizationUserStatusType.Confirmed ||
|
||||||
|
(orgAdminOrgUser.Type != OrganizationUserType.Admin && orgAdminOrgUser.Type != OrganizationUserType.Owner))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Org admin not found.");
|
||||||
|
}
|
||||||
|
var token = _orgDeleteTokenDataFactory.Protect(new OrgDeleteTokenable(organization, 1));
|
||||||
|
await _mailService.SendInitiateDeleteOrganzationEmailAsync(orgAdminEmail, organization, token);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task DeleteAsync(Organization organization)
|
public async Task DeleteAsync(Organization organization)
|
||||||
{
|
{
|
||||||
await ValidateDeleteOrganizationAsync(organization);
|
await ValidateDeleteOrganizationAsync(organization);
|
||||||
@ -1323,7 +1331,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
var validOrganizationUserIds = validOrganizationUsers.Select(u => u.UserId.Value).ToList();
|
var validOrganizationUserIds = validOrganizationUsers.Select(u => u.UserId.Value).ToList();
|
||||||
|
|
||||||
var organization = await GetOrgById(organizationId);
|
var organization = await GetOrgById(organizationId);
|
||||||
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organizationId);
|
|
||||||
var usersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validOrganizationUserIds);
|
var usersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validOrganizationUserIds);
|
||||||
var users = await _userRepository.GetManyAsync(validOrganizationUserIds);
|
var users = await _userRepository.GetManyAsync(validOrganizationUserIds);
|
||||||
|
|
||||||
@ -1355,7 +1362,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await CheckPolicies(policies, organizationId, user, orgUsers, userService);
|
await CheckPolicies(organizationId, user, orgUsers, userService);
|
||||||
orgUser.Status = OrganizationUserStatusType.Confirmed;
|
orgUser.Status = OrganizationUserStatusType.Confirmed;
|
||||||
orgUser.Key = keys[orgUser.Id];
|
orgUser.Key = keys[orgUser.Id];
|
||||||
orgUser.Email = null;
|
orgUser.Email = null;
|
||||||
@ -1449,22 +1456,29 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CheckPolicies(ICollection<Policy> policies, Guid organizationId, User user,
|
private async Task CheckPolicies(Guid organizationId, User user,
|
||||||
ICollection<OrganizationUser> userOrgs, IUserService userService)
|
ICollection<OrganizationUser> userOrgs, IUserService userService)
|
||||||
{
|
{
|
||||||
var usingTwoFactorPolicy = policies.Any(p => p.Type == PolicyType.TwoFactorAuthentication && p.Enabled);
|
// Enforce Two Factor Authentication Policy for this organization
|
||||||
if (usingTwoFactorPolicy && !await userService.TwoFactorIsEnabledAsync(user))
|
var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)).Any(p => p.OrganizationId == organizationId);
|
||||||
|
if (orgRequiresTwoFactor && !await userService.TwoFactorIsEnabledAsync(user))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("User does not have two-step login enabled.");
|
throw new BadRequestException("User does not have two-step login enabled.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var usingSingleOrgPolicy = policies.Any(p => p.Type == PolicyType.SingleOrg && p.Enabled);
|
var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId);
|
||||||
if (usingSingleOrgPolicy)
|
var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg);
|
||||||
|
var otherSingleOrgPolicies =
|
||||||
|
singleOrgPolicies.Where(p => p.OrganizationId != organizationId);
|
||||||
|
// Enforce Single Organization Policy for this organization
|
||||||
|
if (hasOtherOrgs && singleOrgPolicies.Any(p => p.OrganizationId == organizationId))
|
||||||
{
|
{
|
||||||
if (userOrgs.Any(ou => ou.OrganizationId != organizationId && ou.Status != OrganizationUserStatusType.Invited))
|
throw new BadRequestException("Cannot confirm this member to the organization until they leave or remove all other organizations.");
|
||||||
{
|
}
|
||||||
throw new BadRequestException("User is a member of another organization.");
|
// Enforce Single Organization Policy of other organizations user is a member of
|
||||||
}
|
if (otherSingleOrgPolicies.Any())
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot confirm this member to the organization because they are in another organization which forbids it.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1673,14 +1687,14 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, EventSystemUser systemUser, string email,
|
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, EventSystemUser systemUser, string email,
|
||||||
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections,
|
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections,
|
||||||
IEnumerable<Guid> groups)
|
IEnumerable<Guid> groups, bool accessSecretsManager)
|
||||||
{
|
{
|
||||||
// Collection associations validation not required as they are always an empty list - created via system user (scim)
|
// Collection associations validation not required as they are always an empty list - created via system user (scim)
|
||||||
return await SaveUserSendInviteAsync(organizationId, invitingUserId: null, systemUser, email, type, accessAll, externalId, collections, groups);
|
return await SaveUserSendInviteAsync(organizationId, invitingUserId: null, systemUser, email, type, accessAll, externalId, collections, groups, accessSecretsManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<OrganizationUser> SaveUserSendInviteAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser, string email,
|
private async Task<OrganizationUser> SaveUserSendInviteAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser, string email,
|
||||||
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups)
|
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups, bool accessSecretsManager = false)
|
||||||
{
|
{
|
||||||
var invite = new OrganizationUserInvite()
|
var invite = new OrganizationUserInvite()
|
||||||
{
|
{
|
||||||
@ -1688,7 +1702,8 @@ public class OrganizationService : IOrganizationService
|
|||||||
Type = type,
|
Type = type,
|
||||||
AccessAll = accessAll,
|
AccessAll = accessAll,
|
||||||
Collections = collections,
|
Collections = collections,
|
||||||
Groups = groups
|
Groups = groups,
|
||||||
|
AccessSecretsManager = accessSecretsManager
|
||||||
};
|
};
|
||||||
var results = systemUser.HasValue ? await InviteUsersAsync(organizationId, systemUser.Value,
|
var results = systemUser.HasValue ? await InviteUsersAsync(organizationId, systemUser.Value,
|
||||||
new (OrganizationUserInvite, string)[] { (invite, externalId) }) : await InviteUsersAsync(organizationId, invitingUserId,
|
new (OrganizationUserInvite, string)[] { (invite, externalId) }) : await InviteUsersAsync(organizationId, invitingUserId,
|
||||||
@ -1787,6 +1802,8 @@ public class OrganizationService : IOrganizationService
|
|||||||
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
|
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hasStandaloneSecretsManager = await _paymentService.HasSecretsManagerStandalone(organization);
|
||||||
|
|
||||||
var userInvites = new List<(OrganizationUserInvite, string)>();
|
var userInvites = new List<(OrganizationUserInvite, string)>();
|
||||||
foreach (var user in newUsers)
|
foreach (var user in newUsers)
|
||||||
{
|
{
|
||||||
@ -1803,6 +1820,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
Type = OrganizationUserType.User,
|
Type = OrganizationUserType.User,
|
||||||
AccessAll = false,
|
AccessAll = false,
|
||||||
Collections = new List<CollectionAccessSelection>(),
|
Collections = new List<CollectionAccessSelection>(),
|
||||||
|
AccessSecretsManager = hasStandaloneSecretsManager
|
||||||
};
|
};
|
||||||
userInvites.Add((invite, user.ExternalId));
|
userInvites.Add((invite, user.ExternalId));
|
||||||
}
|
}
|
||||||
|
@ -9,4 +9,5 @@ public interface IAuthRequestRepository : IRepository<AuthRequest, Guid>
|
|||||||
Task<ICollection<AuthRequest>> GetManyByUserIdAsync(Guid userId);
|
Task<ICollection<AuthRequest>> GetManyByUserIdAsync(Guid userId);
|
||||||
Task<ICollection<OrganizationAdminAuthRequest>> GetManyPendingByOrganizationIdAsync(Guid organizationId);
|
Task<ICollection<OrganizationAdminAuthRequest>> GetManyPendingByOrganizationIdAsync(Guid organizationId);
|
||||||
Task<ICollection<OrganizationAdminAuthRequest>> GetManyAdminApprovalRequestsByManyIdsAsync(Guid organizationId, IEnumerable<Guid> ids);
|
Task<ICollection<OrganizationAdminAuthRequest>> GetManyAdminApprovalRequestsByManyIdsAsync(Guid organizationId, IEnumerable<Guid> ids);
|
||||||
|
Task UpdateManyAsync(IEnumerable<AuthRequest> authRequests);
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,10 @@ public static class BillingExtensions
|
|||||||
PlanType: PlanType.TeamsMonthly or PlanType.EnterpriseMonthly
|
PlanType: PlanType.TeamsMonthly or PlanType.EnterpriseMonthly
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static bool IsStripeEnabled(this Organization organization)
|
||||||
|
=> !string.IsNullOrEmpty(organization.GatewayCustomerId) &&
|
||||||
|
!string.IsNullOrEmpty(organization.GatewaySubscriptionId);
|
||||||
|
|
||||||
public static bool SupportsConsolidatedBilling(this PlanType planType)
|
public static bool SupportsConsolidatedBilling(this PlanType planType)
|
||||||
=> planType is PlanType.TeamsMonthly or PlanType.EnterpriseMonthly;
|
=> planType is PlanType.TeamsMonthly or PlanType.EnterpriseMonthly;
|
||||||
}
|
}
|
||||||
|
@ -102,11 +102,6 @@ public static class AuthenticationSchemes
|
|||||||
|
|
||||||
public static class FeatureFlagKeys
|
public static class FeatureFlagKeys
|
||||||
{
|
{
|
||||||
public const string DisplayEuEnvironment = "display-eu-environment";
|
|
||||||
public const string DisplayLowKdfIterationWarning = "display-kdf-iteration-warning";
|
|
||||||
public const string PasswordlessLogin = "passwordless-login";
|
|
||||||
public const string TrustedDeviceEncryption = "trusted-device-encryption";
|
|
||||||
public const string Fido2VaultCredentials = "fido2-vault-credentials";
|
|
||||||
public const string VaultOnboarding = "vault-onboarding";
|
public const string VaultOnboarding = "vault-onboarding";
|
||||||
public const string BrowserFilelessImport = "browser-fileless-import";
|
public const string BrowserFilelessImport = "browser-fileless-import";
|
||||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||||
@ -120,10 +115,6 @@ public static class FeatureFlagKeys
|
|||||||
public const string KeyRotationImprovements = "key-rotation-improvements";
|
public const string KeyRotationImprovements = "key-rotation-improvements";
|
||||||
public const string DuoRedirect = "duo-redirect";
|
public const string DuoRedirect = "duo-redirect";
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enables flexible collections improvements for new organizations on creation
|
|
||||||
/// </summary>
|
|
||||||
public const string FlexibleCollectionsSignup = "flexible-collections-signup";
|
|
||||||
/// <summary>
|
|
||||||
/// Exposes a migration button in the web vault which allows users to migrate an existing organization to
|
/// Exposes a migration button in the web vault which allows users to migrate an existing organization to
|
||||||
/// flexible collections
|
/// flexible collections
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -140,6 +131,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string AnhFcmv1Migration = "anh-fcmv1-migration";
|
public const string AnhFcmv1Migration = "anh-fcmv1-migration";
|
||||||
public const string ExtensionRefresh = "extension-refresh";
|
public const string ExtensionRefresh = "extension-refresh";
|
||||||
public const string RestrictProviderAccess = "restrict-provider-access";
|
public const string RestrictProviderAccess = "restrict-provider-access";
|
||||||
|
public const string VaultBulkManagementAction = "vault-bulk-management-action";
|
||||||
|
|
||||||
public static List<string> GetAllKeys()
|
public static List<string> GetAllKeys()
|
||||||
{
|
{
|
||||||
@ -154,11 +146,8 @@ public static class FeatureFlagKeys
|
|||||||
// place overriding values when needed locally (offline), or return null
|
// place overriding values when needed locally (offline), or return null
|
||||||
return new Dictionary<string, string>()
|
return new Dictionary<string, string>()
|
||||||
{
|
{
|
||||||
{ TrustedDeviceEncryption, "true" },
|
|
||||||
{ Fido2VaultCredentials, "true" },
|
|
||||||
{ DuoRedirect, "true" },
|
{ DuoRedirect, "true" },
|
||||||
{ UnassignedItemsBanner, "true"},
|
{ UnassignedItemsBanner, "true"}
|
||||||
{ FlexibleCollectionsSignup, "true" }
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,19 +21,19 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
||||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.300.86" />
|
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.300.93" />
|
||||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.300.86" />
|
<PackageReference Include="AWSSDK.SQS" Version="3.7.301.6" />
|
||||||
<PackageReference Include="Azure.Data.Tables" Version="12.8.3" />
|
<PackageReference Include="Azure.Data.Tables" Version="12.8.3" />
|
||||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.2" />
|
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
||||||
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.15.0" />
|
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.17.5" />
|
||||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.14.1" />
|
<PackageReference Include="Azure.Storage.Blobs" Version="12.19.1" />
|
||||||
<PackageReference Include="Azure.Storage.Queues" Version="12.12.0" />
|
<PackageReference Include="Azure.Storage.Queues" Version="12.17.1" />
|
||||||
<PackageReference Include="BitPay.Light" Version="1.0.1907" />
|
<PackageReference Include="BitPay.Light" Version="1.0.1907" />
|
||||||
<PackageReference Include="DuoUniversal" Version="1.2.3" />
|
<PackageReference Include="DuoUniversal" Version="1.2.4" />
|
||||||
<PackageReference Include="DnsClient" Version="1.7.0" />
|
<PackageReference Include="DnsClient" Version="1.7.0" />
|
||||||
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
|
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
|
||||||
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
|
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
|
||||||
<PackageReference Include="MailKit" Version="4.5.0" />
|
<PackageReference Include="MailKit" Version="4.6.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.25" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.25" />
|
||||||
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.39.1" />
|
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.39.1" />
|
||||||
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
|
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
|
||||||
@ -57,7 +57,7 @@
|
|||||||
<PackageReference Include="Otp.NET" Version="1.3.0" />
|
<PackageReference Include="Otp.NET" Version="1.3.0" />
|
||||||
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="6.0.25" />
|
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="6.0.25" />
|
||||||
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.4.0" />
|
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
{{#>FullHtmlLayout}}
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<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; text-align: left;" valign="top" align="center">
|
||||||
|
We recently received your request to permanently delete the following Bitwarden organization:
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||||
|
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Name:</b> {{OrganizationName}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">ID:</b> {{OrganizationId}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Created:</b> {{OrganizationCreationDate}} at {{OrganizationCreationTime}} {{TimeZone}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Plan:</b> {{OrganizationPlan}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Number of seats:</b> {{OrganizationSeats}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Billing email address:</b> {{OrganizationBillingEmail}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<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; text-align: left;" valign="top" align="center">
|
||||||
|
Click the link below to delete your Bitwarden organization.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block last" 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; -webkit-text-size-adjust: none; text-align: left;" valign="top" align="center">
|
||||||
|
If you did not request this email to delete your Bitwarden organization, please contact us.
|
||||||
|
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<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; text-align: center;" valign="top" align="center">
|
||||||
|
<a href="{{{Url}}}" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
Delete Your Organization
|
||||||
|
</a>
|
||||||
|
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{{/FullHtmlLayout}}
|
@ -0,0 +1,17 @@
|
|||||||
|
{{#>BasicTextLayout}}
|
||||||
|
We recently received your request to permanently delete the following Bitwarden organization:
|
||||||
|
|
||||||
|
- Name: {{OrganizationName}}
|
||||||
|
- ID: {{OrganizationId}}
|
||||||
|
- Created: {{OrganizationCreationDate}} at {{OrganizationCreationTime}} {{TimeZone}}
|
||||||
|
- Plan: {{OrganizationPlan}}
|
||||||
|
- Number of seats: {{OrganizationSeats}}
|
||||||
|
- Billing email address: {{OrganizationBillingEmail}}
|
||||||
|
|
||||||
|
Click the link below to complete the deletion of your organization.
|
||||||
|
|
||||||
|
If you did not request this email to delete your Bitwarden organization, please contact us.
|
||||||
|
|
||||||
|
{{{Url}}}
|
||||||
|
|
||||||
|
{{/BasicTextLayout}}
|
@ -14,4 +14,9 @@ public class CollectionAdminDetails : CollectionDetails
|
|||||||
/// Flag for whether the user has been explicitly assigned to the collection either directly or through a group.
|
/// Flag for whether the user has been explicitly assigned to the collection either directly or through a group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Assigned { get; set; }
|
public bool Assigned { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flag for whether a collection is managed by an active user or group.
|
||||||
|
/// </summary>
|
||||||
|
public bool Unmanaged { get; set; }
|
||||||
}
|
}
|
||||||
|
23
src/Core/Models/Mail/OrganizationInitiateDeleteModel.cs
Normal file
23
src/Core/Models/Mail/OrganizationInitiateDeleteModel.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using Bit.Core.Models.Mail;
|
||||||
|
|
||||||
|
namespace Bit.Core.Auth.Models.Mail;
|
||||||
|
|
||||||
|
public class OrganizationInitiateDeleteModel : BaseMailModel
|
||||||
|
{
|
||||||
|
public string Url => string.Format("{0}/verify-recover-delete-org?orgId={1}&token={2}&name={3}",
|
||||||
|
WebVaultUrl,
|
||||||
|
OrganizationId,
|
||||||
|
Token,
|
||||||
|
OrganizationNameUrlEncoded);
|
||||||
|
|
||||||
|
public string Token { get; set; }
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
public string OrganizationName { get; set; }
|
||||||
|
public string OrganizationNameUrlEncoded { get; set; }
|
||||||
|
public string OrganizationPlan { get; set; }
|
||||||
|
public string OrganizationSeats { get; set; }
|
||||||
|
public string OrganizationBillingEmail { get; set; }
|
||||||
|
public string OrganizationCreationDate { get; set; }
|
||||||
|
public string OrganizationCreationTime { get; set; }
|
||||||
|
public string TimeZone { get; set; }
|
||||||
|
}
|
@ -28,6 +28,8 @@ public record TeamsStarterPlan : Plan
|
|||||||
|
|
||||||
PasswordManager = new TeamsStarterPasswordManagerFeatures();
|
PasswordManager = new TeamsStarterPasswordManagerFeatures();
|
||||||
SecretsManager = new TeamsStarterSecretsManagerFeatures();
|
SecretsManager = new TeamsStarterSecretsManagerFeatures();
|
||||||
|
|
||||||
|
LegacyYear = 2024;
|
||||||
}
|
}
|
||||||
|
|
||||||
private record TeamsStarterSecretsManagerFeatures : SecretsManagerPlanFeatures
|
private record TeamsStarterSecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||||
|
@ -79,5 +79,6 @@ public interface IMailService
|
|||||||
Task SendTrustedDeviceAdminApprovalEmailAsync(string email, DateTime utcNow, string ip, string deviceTypeAndIdentifier);
|
Task SendTrustedDeviceAdminApprovalEmailAsync(string email, DateTime utcNow, string ip, string deviceTypeAndIdentifier);
|
||||||
Task SendTrialInitiationEmailAsync(string email);
|
Task SendTrialInitiationEmailAsync(string email);
|
||||||
Task SendInitiateDeletProviderEmailAsync(string email, Provider provider, string token);
|
Task SendInitiateDeletProviderEmailAsync(string email, Provider provider, string token);
|
||||||
|
Task SendInitiateDeleteOrganzationEmailAsync(string email, Organization organization, string token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,4 +57,5 @@ public interface IPaymentService
|
|||||||
Task<string> AddSecretsManagerToSubscription(Organization org, Plan plan, int additionalSmSeats,
|
Task<string> AddSecretsManagerToSubscription(Organization org, Plan plan, int additionalSmSeats,
|
||||||
int additionalServiceAccount, DateTime? prorationDate = null);
|
int additionalServiceAccount, DateTime? prorationDate = null);
|
||||||
Task<bool> RisksSubscriptionFailure(Organization organization);
|
Task<bool> RisksSubscriptionFailure(Organization organization);
|
||||||
|
Task<bool> HasSecretsManagerStandalone(Organization organization);
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,6 @@ using Bit.Core.Auth.Models;
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Tools.Entities;
|
|
||||||
using Bit.Core.Vault.Entities;
|
|
||||||
using Fido2NetLib;
|
using Fido2NetLib;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
@ -39,8 +37,6 @@ public interface IUserService
|
|||||||
Task<IdentityResult> UpdateTempPasswordAsync(User user, string newMasterPassword, string key, string hint);
|
Task<IdentityResult> UpdateTempPasswordAsync(User user, string newMasterPassword, string key, string hint);
|
||||||
Task<IdentityResult> ChangeKdfAsync(User user, string masterPassword, string newMasterPassword, string key,
|
Task<IdentityResult> ChangeKdfAsync(User user, string masterPassword, string newMasterPassword, string key,
|
||||||
KdfType kdf, int kdfIterations, int? kdfMemory, int? kdfParallelism);
|
KdfType kdf, int kdfIterations, int? kdfMemory, int? kdfParallelism);
|
||||||
Task<IdentityResult> UpdateKeyAsync(User user, string masterPassword, string key, string privateKey,
|
|
||||||
IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders, IEnumerable<Send> sends);
|
|
||||||
Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash);
|
Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash);
|
||||||
Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type, bool setEnabled = true, bool logEvent = true);
|
Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type, bool setEnabled = true, bool logEvent = true);
|
||||||
Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type,
|
Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type,
|
||||||
|
@ -1002,6 +1002,30 @@ public class HandlebarsMailService : IMailService
|
|||||||
await _mailDeliveryService.SendEmailAsync(message);
|
await _mailDeliveryService.SendEmailAsync(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SendInitiateDeleteOrganzationEmailAsync(string email, Organization organization, string token)
|
||||||
|
{
|
||||||
|
var message = CreateDefaultMessage("Request to Delete Your Organization", email);
|
||||||
|
var model = new OrganizationInitiateDeleteModel
|
||||||
|
{
|
||||||
|
Token = WebUtility.UrlEncode(token),
|
||||||
|
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||||
|
SiteName = _globalSettings.SiteName,
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
OrganizationName = CoreHelpers.SanitizeForEmail(organization.DisplayName(), false),
|
||||||
|
OrganizationNameUrlEncoded = WebUtility.UrlEncode(organization.Name),
|
||||||
|
OrganizationBillingEmail = organization.BillingEmail,
|
||||||
|
OrganizationPlan = organization.Plan,
|
||||||
|
OrganizationSeats = organization.Seats.ToString(),
|
||||||
|
OrganizationCreationDate = organization.CreationDate.ToLongDateString(),
|
||||||
|
OrganizationCreationTime = organization.CreationDate.ToShortTimeString(),
|
||||||
|
TimeZone = _utcTimeZoneDisplay,
|
||||||
|
};
|
||||||
|
await AddMessageContentAsync(message, "InitiateDeleteOrganzation", model);
|
||||||
|
message.MetaData.Add("SendGridBypassListManagement", true);
|
||||||
|
message.Category = "InitiateDeleteOrganzation";
|
||||||
|
await _mailDeliveryService.SendEmailAsync(message);
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetUserIdentifier(string email, string userName)
|
private static string GetUserIdentifier(string email, string userName)
|
||||||
{
|
{
|
||||||
return string.IsNullOrEmpty(userName) ? email : CoreHelpers.SanitizeForEmail(userName, false);
|
return string.IsNullOrEmpty(userName) ? email : CoreHelpers.SanitizeForEmail(userName, false);
|
||||||
|
@ -1800,6 +1800,18 @@ public class StripePaymentService : IPaymentService
|
|||||||
return paymentSource == null;
|
return paymentSource == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> HasSecretsManagerStandalone(Organization organization)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(organization.GatewayCustomerId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var customer = await _stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId);
|
||||||
|
|
||||||
|
return customer?.Discount?.Coupon?.Id == SecretsManagerStandaloneDiscountId;
|
||||||
|
}
|
||||||
|
|
||||||
private PaymentMethod GetLatestCardPaymentMethod(string customerId)
|
private PaymentMethod GetLatestCardPaymentMethod(string customerId)
|
||||||
{
|
{
|
||||||
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(
|
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(
|
||||||
|
@ -14,12 +14,10 @@ using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
using Bit.Core.Tools.Entities;
|
|
||||||
using Bit.Core.Tools.Enums;
|
using Bit.Core.Tools.Enums;
|
||||||
using Bit.Core.Tools.Models.Business;
|
using Bit.Core.Tools.Models.Business;
|
||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tools.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Vault.Entities;
|
|
||||||
using Bit.Core.Vault.Repositories;
|
using Bit.Core.Vault.Repositories;
|
||||||
using Fido2NetLib;
|
using Fido2NetLib;
|
||||||
using Fido2NetLib.Objects;
|
using Fido2NetLib.Objects;
|
||||||
@ -862,39 +860,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
|
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IdentityResult> UpdateKeyAsync(User user, string masterPassword, string key, string privateKey,
|
|
||||||
IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders, IEnumerable<Send> sends)
|
|
||||||
{
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await CheckPasswordAsync(user, masterPassword))
|
|
||||||
{
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
user.RevisionDate = user.AccountRevisionDate = now;
|
|
||||||
user.LastKeyRotationDate = now;
|
|
||||||
user.SecurityStamp = Guid.NewGuid().ToString();
|
|
||||||
user.Key = key;
|
|
||||||
user.PrivateKey = privateKey;
|
|
||||||
if (ciphers.Any() || folders.Any() || sends.Any())
|
|
||||||
{
|
|
||||||
await _cipherRepository.UpdateUserKeysAndCiphersAsync(user, ciphers, folders, sends);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _userRepository.ReplaceAsync(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _pushService.PushLogOutAsync(user.Id, excludeCurrentContextFromPush: true);
|
|
||||||
return IdentityResult.Success;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.LogWarning("Update key failed for user {userId}.", user.Id);
|
|
||||||
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IdentityResult> RefreshSecurityStampAsync(User user, string secret)
|
public async Task<IdentityResult> RefreshSecurityStampAsync(User user, string secret)
|
||||||
{
|
{
|
||||||
if (user == null)
|
if (user == null)
|
||||||
|
@ -270,5 +270,10 @@ public class NoopMailService : IMailService
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Task SendInitiateDeletProviderEmailAsync(string email, Provider provider, string token) => throw new NotImplementedException();
|
public Task SendInitiateDeletProviderEmailAsync(string email, Provider provider, string token) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task SendInitiateDeleteOrganzationEmailAsync(string email, Organization organization, string token)
|
||||||
|
{
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,4 +8,6 @@ public enum ReferenceEventSource
|
|||||||
Organization,
|
Organization,
|
||||||
[EnumMember(Value = "user")]
|
[EnumMember(Value = "user")]
|
||||||
User,
|
User,
|
||||||
|
[EnumMember(Value = "provider")]
|
||||||
|
Provider,
|
||||||
}
|
}
|
||||||
|
16
src/Core/Utilities/ModelStateExtensions.cs
Normal file
16
src/Core/Utilities/ModelStateExtensions.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
|
||||||
|
namespace Bit.Core.Utilities;
|
||||||
|
|
||||||
|
public static class ModelStateExtensions
|
||||||
|
{
|
||||||
|
public static string GetErrorMessage(this ModelStateDictionary modelState)
|
||||||
|
{
|
||||||
|
var errors = modelState.Values
|
||||||
|
.SelectMany(v => v.Errors)
|
||||||
|
.Select(e => e.ErrorMessage)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return string.Join("; ", errors);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
using Bit.Core.Auth.UserFeatures.UserKey;
|
using Bit.Core.Auth.UserFeatures.UserKey;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Tools.Entities;
|
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Bit.Core.Vault.Models.Data;
|
using Bit.Core.Vault.Models.Data;
|
||||||
|
|
||||||
@ -30,7 +29,6 @@ public interface ICipherRepository : IRepository<Cipher, Guid>
|
|||||||
Task MoveAsync(IEnumerable<Guid> ids, Guid? folderId, Guid userId, bool useFlexibleCollections);
|
Task MoveAsync(IEnumerable<Guid> ids, Guid? folderId, Guid userId, bool useFlexibleCollections);
|
||||||
Task DeleteByUserIdAsync(Guid userId);
|
Task DeleteByUserIdAsync(Guid userId);
|
||||||
Task DeleteByOrganizationIdAsync(Guid organizationId);
|
Task DeleteByOrganizationIdAsync(Guid organizationId);
|
||||||
Task UpdateUserKeysAndCiphersAsync(User user, IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders, IEnumerable<Send> sends);
|
|
||||||
Task UpdateCiphersAsync(Guid userId, IEnumerable<Cipher> ciphers);
|
Task UpdateCiphersAsync(Guid userId, IEnumerable<Cipher> ciphers);
|
||||||
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);
|
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);
|
||||||
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
|
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
|
||||||
|
@ -14,8 +14,6 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
|
|
||||||
namespace Bit.Identity.Controllers;
|
namespace Bit.Identity.Controllers;
|
||||||
|
|
||||||
// TODO: 2023-10-16, Remove account alias (https://bitwarden.atlassian.net/browse/PM-1247)
|
|
||||||
[Route("account/[action]")]
|
|
||||||
[Route("sso/[action]")]
|
[Route("sso/[action]")]
|
||||||
public class SsoController : Controller
|
public class SsoController : Controller
|
||||||
{
|
{
|
||||||
|
@ -39,6 +39,7 @@ public static class ServiceCollectionExtensions
|
|||||||
}
|
}
|
||||||
options.InputLengthRestrictions.UserName = 256;
|
options.InputLengthRestrictions.UserName = 256;
|
||||||
options.KeyManagement.Enabled = false;
|
options.KeyManagement.Enabled = false;
|
||||||
|
options.UserInteraction.LoginUrl = "/sso/Login";
|
||||||
})
|
})
|
||||||
.AddInMemoryCaching()
|
.AddInMemoryCaching()
|
||||||
.AddInMemoryApiResources(ApiResources.GetApiResources())
|
.AddInMemoryApiResources(ApiResources.GetApiResources())
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
|
using System.Text.Json;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -74,4 +75,20 @@ public class AuthRequestRepository : Repository<AuthRequest, Guid>, IAuthRequest
|
|||||||
return results.ToList();
|
return results.ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task UpdateManyAsync(IEnumerable<AuthRequest> authRequests)
|
||||||
|
{
|
||||||
|
if (!authRequests.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var connection = new SqlConnection(ConnectionString))
|
||||||
|
{
|
||||||
|
var results = await connection.ExecuteAsync(
|
||||||
|
$"[dbo].[AuthRequest_UpdateMany]",
|
||||||
|
new { jsonData = JsonSerializer.Serialize(authRequests) },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -380,170 +380,6 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task UpdateUserKeysAndCiphersAsync(User user, IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders, IEnumerable<Send> sends)
|
|
||||||
{
|
|
||||||
using (var connection = new SqlConnection(ConnectionString))
|
|
||||||
{
|
|
||||||
connection.Open();
|
|
||||||
|
|
||||||
using (var transaction = connection.BeginTransaction())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// 1. Update user.
|
|
||||||
|
|
||||||
using (var cmd = new SqlCommand("[dbo].[User_UpdateKeys]", connection, transaction))
|
|
||||||
{
|
|
||||||
cmd.CommandType = CommandType.StoredProcedure;
|
|
||||||
cmd.Parameters.Add("@Id", SqlDbType.UniqueIdentifier).Value = user.Id;
|
|
||||||
cmd.Parameters.Add("@SecurityStamp", SqlDbType.NVarChar).Value = user.SecurityStamp;
|
|
||||||
cmd.Parameters.Add("@Key", SqlDbType.VarChar).Value = user.Key;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(user.PrivateKey))
|
|
||||||
{
|
|
||||||
cmd.Parameters.Add("@PrivateKey", SqlDbType.VarChar).Value = DBNull.Value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
cmd.Parameters.Add("@PrivateKey", SqlDbType.VarChar).Value = user.PrivateKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Parameters.Add("@RevisionDate", SqlDbType.DateTime2).Value = user.RevisionDate;
|
|
||||||
cmd.Parameters.Add("@AccountRevisionDate", SqlDbType.DateTime2).Value = user.AccountRevisionDate;
|
|
||||||
cmd.Parameters.Add("@LastKeyRotationDate", SqlDbType.DateTime2).Value = user.LastKeyRotationDate;
|
|
||||||
cmd.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Create temp tables to bulk copy into.
|
|
||||||
|
|
||||||
var sqlCreateTemp = @"
|
|
||||||
SELECT TOP 0 *
|
|
||||||
INTO #TempCipher
|
|
||||||
FROM [dbo].[Cipher]
|
|
||||||
|
|
||||||
SELECT TOP 0 *
|
|
||||||
INTO #TempFolder
|
|
||||||
FROM [dbo].[Folder]
|
|
||||||
|
|
||||||
SELECT TOP 0 *
|
|
||||||
INTO #TempSend
|
|
||||||
FROM [dbo].[Send]";
|
|
||||||
|
|
||||||
using (var cmd = new SqlCommand(sqlCreateTemp, connection, transaction))
|
|
||||||
{
|
|
||||||
cmd.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Bulk copy into temp tables.
|
|
||||||
|
|
||||||
if (ciphers.Any())
|
|
||||||
{
|
|
||||||
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
|
|
||||||
{
|
|
||||||
bulkCopy.DestinationTableName = "#TempCipher";
|
|
||||||
var dataTable = BuildCiphersTable(bulkCopy, ciphers);
|
|
||||||
bulkCopy.WriteToServer(dataTable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (folders.Any())
|
|
||||||
{
|
|
||||||
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
|
|
||||||
{
|
|
||||||
bulkCopy.DestinationTableName = "#TempFolder";
|
|
||||||
var dataTable = BuildFoldersTable(bulkCopy, folders);
|
|
||||||
bulkCopy.WriteToServer(dataTable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sends.Any())
|
|
||||||
{
|
|
||||||
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
|
|
||||||
{
|
|
||||||
bulkCopy.DestinationTableName = "#TempSend";
|
|
||||||
var dataTable = BuildSendsTable(bulkCopy, sends);
|
|
||||||
bulkCopy.WriteToServer(dataTable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Insert into real tables from temp tables and clean up.
|
|
||||||
|
|
||||||
var sql = string.Empty;
|
|
||||||
|
|
||||||
if (ciphers.Any())
|
|
||||||
{
|
|
||||||
sql += @"
|
|
||||||
UPDATE
|
|
||||||
[dbo].[Cipher]
|
|
||||||
SET
|
|
||||||
[Data] = TC.[Data],
|
|
||||||
[Attachments] = TC.[Attachments],
|
|
||||||
[RevisionDate] = TC.[RevisionDate],
|
|
||||||
[Key] = TC.[Key]
|
|
||||||
FROM
|
|
||||||
[dbo].[Cipher] C
|
|
||||||
INNER JOIN
|
|
||||||
#TempCipher TC ON C.Id = TC.Id
|
|
||||||
WHERE
|
|
||||||
C.[UserId] = @UserId";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (folders.Any())
|
|
||||||
{
|
|
||||||
sql += @"
|
|
||||||
UPDATE
|
|
||||||
[dbo].[Folder]
|
|
||||||
SET
|
|
||||||
[Name] = TF.[Name],
|
|
||||||
[RevisionDate] = TF.[RevisionDate]
|
|
||||||
FROM
|
|
||||||
[dbo].[Folder] F
|
|
||||||
INNER JOIN
|
|
||||||
#TempFolder TF ON F.Id = TF.Id
|
|
||||||
WHERE
|
|
||||||
F.[UserId] = @UserId";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sends.Any())
|
|
||||||
{
|
|
||||||
sql += @"
|
|
||||||
UPDATE
|
|
||||||
[dbo].[Send]
|
|
||||||
SET
|
|
||||||
[Key] = TS.[Key],
|
|
||||||
[RevisionDate] = TS.[RevisionDate]
|
|
||||||
FROM
|
|
||||||
[dbo].[Send] S
|
|
||||||
INNER JOIN
|
|
||||||
#TempSend TS ON S.Id = TS.Id
|
|
||||||
WHERE
|
|
||||||
S.[UserId] = @UserId";
|
|
||||||
}
|
|
||||||
|
|
||||||
sql += @"
|
|
||||||
DROP TABLE #TempCipher
|
|
||||||
DROP TABLE #TempFolder
|
|
||||||
DROP TABLE #TempSend";
|
|
||||||
|
|
||||||
using (var cmd = new SqlCommand(sql, connection, transaction))
|
|
||||||
{
|
|
||||||
cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = user.Id;
|
|
||||||
cmd.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction.Commit();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
transaction.Rollback();
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateCiphersAsync(Guid userId, IEnumerable<Cipher> ciphers)
|
public async Task UpdateCiphersAsync(Guid userId, IEnumerable<Cipher> ciphers)
|
||||||
{
|
{
|
||||||
if (!ciphers.Any())
|
if (!ciphers.Any())
|
||||||
|
@ -69,4 +69,29 @@ public class AuthRequestRepository : Repository<Core.Auth.Entities.AuthRequest,
|
|||||||
return orgUserAuthRequests;
|
return orgUserAuthRequests;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task UpdateManyAsync(IEnumerable<Core.Auth.Entities.AuthRequest> authRequests)
|
||||||
|
{
|
||||||
|
if (!authRequests.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entities = new List<AuthRequest>();
|
||||||
|
foreach (var authRequest in authRequests)
|
||||||
|
{
|
||||||
|
if (!authRequest.Id.Equals(default))
|
||||||
|
{
|
||||||
|
var entity = Mapper.Map<AuthRequest>(authRequest);
|
||||||
|
entities.Add(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
dbContext.UpdateRange(entities);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -340,7 +340,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
|||||||
ExternalId = collectionGroup.Key.ExternalId,
|
ExternalId = collectionGroup.Key.ExternalId,
|
||||||
ReadOnly = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.ReadOnly))),
|
ReadOnly = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.ReadOnly))),
|
||||||
HidePasswords = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
HidePasswords = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
||||||
Manage = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.Manage))),
|
Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
@ -365,7 +365,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
|||||||
ExternalId = collectionGroup.Key.ExternalId,
|
ExternalId = collectionGroup.Key.ExternalId,
|
||||||
ReadOnly = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.ReadOnly))),
|
ReadOnly = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.ReadOnly))),
|
||||||
HidePasswords = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
HidePasswords = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
||||||
Manage = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.Manage))),
|
Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),
|
||||||
}).ToListAsync();
|
}).ToListAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -391,7 +391,8 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
|||||||
c.Name,
|
c.Name,
|
||||||
c.CreationDate,
|
c.CreationDate,
|
||||||
c.RevisionDate,
|
c.RevisionDate,
|
||||||
c.ExternalId
|
c.ExternalId,
|
||||||
|
c.Unmanaged
|
||||||
}).Select(collectionGroup => new CollectionAdminDetails
|
}).Select(collectionGroup => new CollectionAdminDetails
|
||||||
{
|
{
|
||||||
Id = collectionGroup.Key.Id,
|
Id = collectionGroup.Key.Id,
|
||||||
@ -404,7 +405,8 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
|||||||
HidePasswords =
|
HidePasswords =
|
||||||
Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
||||||
Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),
|
Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),
|
||||||
Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned)))
|
Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))),
|
||||||
|
Unmanaged = collectionGroup.Key.Unmanaged
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -417,7 +419,8 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
|||||||
c.Name,
|
c.Name,
|
||||||
c.CreationDate,
|
c.CreationDate,
|
||||||
c.RevisionDate,
|
c.RevisionDate,
|
||||||
c.ExternalId
|
c.ExternalId,
|
||||||
|
c.Unmanaged
|
||||||
}
|
}
|
||||||
into collectionGroup
|
into collectionGroup
|
||||||
select new CollectionAdminDetails
|
select new CollectionAdminDetails
|
||||||
@ -432,7 +435,8 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
|||||||
HidePasswords =
|
HidePasswords =
|
||||||
Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
||||||
Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),
|
Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),
|
||||||
Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned)))
|
Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))),
|
||||||
|
Unmanaged = collectionGroup.Key.Unmanaged
|
||||||
}).ToListAsync();
|
}).ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -511,7 +515,8 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
|||||||
HidePasswords =
|
HidePasswords =
|
||||||
Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
||||||
Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),
|
Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),
|
||||||
Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned)))
|
Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))),
|
||||||
|
Unmanaged = collectionGroup.Select(c => c.Unmanaged).FirstOrDefault()
|
||||||
}).FirstOrDefault();
|
}).FirstOrDefault();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -539,7 +544,8 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
|||||||
HidePasswords =
|
HidePasswords =
|
||||||
Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
||||||
Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),
|
Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),
|
||||||
Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned)))
|
Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))),
|
||||||
|
Unmanaged = collectionGroup.Select(c => c.Unmanaged).FirstOrDefault()
|
||||||
}).FirstOrDefaultAsync();
|
}).FirstOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
||||||
|
|
||||||
@ -46,6 +47,17 @@ public class CollectionAdminDetailsQuery : IQuery<CollectionAdminDetails>
|
|||||||
from cg in cg_g.DefaultIfEmpty()
|
from cg in cg_g.DefaultIfEmpty()
|
||||||
select new { c, cu, cg };
|
select new { c, cu, cg };
|
||||||
|
|
||||||
|
// Subqueries to determine if a colection is managed by an active user or group.
|
||||||
|
var activeUserManageRights = from cu in dbContext.CollectionUsers
|
||||||
|
join ou in dbContext.OrganizationUsers
|
||||||
|
on cu.OrganizationUserId equals ou.Id
|
||||||
|
where ou.Status == OrganizationUserStatusType.Confirmed && cu.Manage
|
||||||
|
select cu.CollectionId;
|
||||||
|
|
||||||
|
var activeGroupManageRights = from cg in dbContext.CollectionGroups
|
||||||
|
where cg.Manage
|
||||||
|
select cg.CollectionId;
|
||||||
|
|
||||||
if (_organizationId.HasValue)
|
if (_organizationId.HasValue)
|
||||||
{
|
{
|
||||||
baseCollectionQuery = baseCollectionQuery.Where(x => x.c.OrganizationId == _organizationId);
|
baseCollectionQuery = baseCollectionQuery.Where(x => x.c.OrganizationId == _organizationId);
|
||||||
@ -71,6 +83,7 @@ public class CollectionAdminDetailsQuery : IQuery<CollectionAdminDetails>
|
|||||||
HidePasswords = (bool?)x.cu.HidePasswords ?? (bool?)x.cg.HidePasswords ?? false,
|
HidePasswords = (bool?)x.cu.HidePasswords ?? (bool?)x.cg.HidePasswords ?? false,
|
||||||
Manage = (bool?)x.cu.Manage ?? (bool?)x.cg.Manage ?? false,
|
Manage = (bool?)x.cu.Manage ?? (bool?)x.cg.Manage ?? false,
|
||||||
Assigned = x.cu != null || x.cg != null,
|
Assigned = x.cu != null || x.cg != null,
|
||||||
|
Unmanaged = !activeUserManageRights.Contains(x.c.Id) && !activeGroupManageRights.Contains(x.c.Id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using NS = Newtonsoft.Json;
|
using NS = Newtonsoft.Json;
|
||||||
using NSL = Newtonsoft.Json.Linq;
|
using NSL = Newtonsoft.Json.Linq;
|
||||||
using User = Bit.Core.Entities.User;
|
|
||||||
|
|
||||||
namespace Bit.Infrastructure.EntityFramework.Vault.Repositories;
|
namespace Bit.Infrastructure.EntityFramework.Vault.Repositories;
|
||||||
|
|
||||||
@ -865,23 +864,6 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task UpdateUserKeysAndCiphersAsync(User user, IEnumerable<Core.Vault.Entities.Cipher> ciphers, IEnumerable<Core.Vault.Entities.Folder> folders, IEnumerable<Core.Tools.Entities.Send> sends)
|
|
||||||
{
|
|
||||||
using (var scope = ServiceScopeFactory.CreateScope())
|
|
||||||
{
|
|
||||||
var dbContext = GetDatabaseContext(scope);
|
|
||||||
await UserUpdateKeys(user);
|
|
||||||
var cipherEntities = Mapper.Map<List<Cipher>>(ciphers);
|
|
||||||
await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, cipherEntities);
|
|
||||||
var folderEntities = Mapper.Map<List<Folder>>(folders);
|
|
||||||
await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, folderEntities);
|
|
||||||
var sendEntities = Mapper.Map<List<Send>>(sends);
|
|
||||||
await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, sendEntities);
|
|
||||||
await dbContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpsertAsync(CipherDetails cipher)
|
public async Task UpsertAsync(CipherDetails cipher)
|
||||||
{
|
{
|
||||||
if (cipher.Id.Equals(default))
|
if (cipher.Id.Equals(default))
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Notifications' " />
|
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Notifications' " />
|
||||||
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Notifications-SelfHost' " />
|
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Notifications-SelfHost' " />
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="8.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="8.0.5" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="8.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="8.0.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -150,6 +150,13 @@ public static class ServiceCollectionExtensions
|
|||||||
|
|
||||||
public static void AddTokenizers(this IServiceCollection services)
|
public static void AddTokenizers(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
|
services.AddSingleton<IDataProtectorTokenFactory<OrgDeleteTokenable>>(serviceProvider =>
|
||||||
|
new DataProtectorTokenFactory<OrgDeleteTokenable>(
|
||||||
|
OrgDeleteTokenable.ClearTextPrefix,
|
||||||
|
OrgDeleteTokenable.DataProtectorPurpose,
|
||||||
|
serviceProvider.GetDataProtectionProvider(),
|
||||||
|
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<OrgDeleteTokenable>>>())
|
||||||
|
);
|
||||||
services.AddSingleton<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>(serviceProvider =>
|
services.AddSingleton<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>(serviceProvider =>
|
||||||
new DataProtectorTokenFactory<EmergencyAccessInviteTokenable>(
|
new DataProtectorTokenFactory<EmergencyAccessInviteTokenable>(
|
||||||
EmergencyAccessInviteTokenable.ClearTextPrefix,
|
EmergencyAccessInviteTokenable.ClearTextPrefix,
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
CREATE PROCEDURE AuthRequest_UpdateMany
|
||||||
|
@jsonData NVARCHAR(MAX)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
UPDATE AR
|
||||||
|
SET
|
||||||
|
[Id] = ARI.[Id],
|
||||||
|
[UserId] = ARI.[UserId],
|
||||||
|
[Type] = ARI.[Type],
|
||||||
|
[RequestDeviceIdentifier] = ARI.[RequestDeviceIdentifier],
|
||||||
|
[RequestDeviceType] = ARI.[RequestDeviceType],
|
||||||
|
[RequestIpAddress] = ARI.[RequestIpAddress],
|
||||||
|
[ResponseDeviceId] = ARI.[ResponseDeviceId],
|
||||||
|
[AccessCode] = ARI.[AccessCode],
|
||||||
|
[PublicKey] = ARI.[PublicKey],
|
||||||
|
[Key] = ARI.[Key],
|
||||||
|
[MasterPasswordHash] = ARI.[MasterPasswordHash],
|
||||||
|
[Approved] = ARI.[Approved],
|
||||||
|
[CreationDate] = ARI.[CreationDate],
|
||||||
|
[ResponseDate] = ARI.[ResponseDate],
|
||||||
|
[AuthenticationDate] = ARI.[AuthenticationDate],
|
||||||
|
[OrganizationId] = ARI.[OrganizationId]
|
||||||
|
FROM
|
||||||
|
[dbo].[AuthRequest] AR
|
||||||
|
INNER JOIN
|
||||||
|
OPENJSON(@jsonData)
|
||||||
|
WITH (
|
||||||
|
Id UNIQUEIDENTIFIER '$.Id',
|
||||||
|
UserId UNIQUEIDENTIFIER '$.UserId',
|
||||||
|
Type SMALLINT '$.Type',
|
||||||
|
RequestDeviceIdentifier NVARCHAR(50) '$.RequestDeviceIdentifier',
|
||||||
|
RequestDeviceType SMALLINT '$.RequestDeviceType',
|
||||||
|
RequestIpAddress VARCHAR(50) '$.RequestIpAddress',
|
||||||
|
ResponseDeviceId UNIQUEIDENTIFIER '$.ResponseDeviceId',
|
||||||
|
AccessCode VARCHAR(25) '$.AccessCode',
|
||||||
|
PublicKey VARCHAR(MAX) '$.PublicKey',
|
||||||
|
[Key] VARCHAR(MAX) '$.Key',
|
||||||
|
MasterPasswordHash VARCHAR(MAX) '$.MasterPasswordHash',
|
||||||
|
Approved BIT '$.Approved',
|
||||||
|
CreationDate DATETIME2 '$.CreationDate',
|
||||||
|
ResponseDate DATETIME2 '$.ResponseDate',
|
||||||
|
AuthenticationDate DATETIME2 '$.AuthenticationDate',
|
||||||
|
OrganizationId UNIQUEIDENTIFIER '$.OrganizationId'
|
||||||
|
) ARI ON AR.Id = ARI.Id;
|
||||||
|
END
|
@ -31,7 +31,29 @@ BEGIN
|
|||||||
CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL
|
CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL
|
||||||
THEN 0
|
THEN 0
|
||||||
ELSE 1
|
ELSE 1
|
||||||
END) AS [Assigned]
|
END) AS [Assigned],
|
||||||
|
CASE
|
||||||
|
WHEN
|
||||||
|
-- No active user or group has manage rights
|
||||||
|
NOT EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM [dbo].[CollectionUser] CU2
|
||||||
|
JOIN [dbo].[OrganizationUser] OU2 ON CU2.[OrganizationUserId] = OU2.[Id]
|
||||||
|
WHERE
|
||||||
|
CU2.[CollectionId] = C.[Id] AND
|
||||||
|
OU2.[Status] = 2 AND
|
||||||
|
CU2.[Manage] = 1
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM [dbo].[CollectionGroup] CG2
|
||||||
|
WHERE
|
||||||
|
CG2.[CollectionId] = C.[Id] AND
|
||||||
|
CG2.[Manage] = 1
|
||||||
|
)
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS [Unmanaged]
|
||||||
FROM
|
FROM
|
||||||
[dbo].[CollectionView] C
|
[dbo].[CollectionView] C
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
|
@ -31,7 +31,29 @@ BEGIN
|
|||||||
CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL
|
CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL
|
||||||
THEN 0
|
THEN 0
|
||||||
ELSE 1
|
ELSE 1
|
||||||
END) AS [Assigned]
|
END) AS [Assigned],
|
||||||
|
CASE
|
||||||
|
WHEN
|
||||||
|
-- No active user or group has manage rights
|
||||||
|
NOT EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM [dbo].[CollectionUser] CU2
|
||||||
|
JOIN [dbo].[OrganizationUser] OU2 ON CU2.[OrganizationUserId] = OU2.[Id]
|
||||||
|
WHERE
|
||||||
|
CU2.[CollectionId] = C.[Id] AND
|
||||||
|
OU2.[Status] = 2 AND
|
||||||
|
CU2.[Manage] = 1
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM [dbo].[CollectionGroup] CG2
|
||||||
|
WHERE
|
||||||
|
CG2.[CollectionId] = C.[Id] AND
|
||||||
|
CG2.[Manage] = 1
|
||||||
|
)
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS [Unmanaged]
|
||||||
FROM
|
FROM
|
||||||
[dbo].[CollectionView] C
|
[dbo].[CollectionView] C
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
|
@ -13,7 +13,7 @@ BEGIN
|
|||||||
ExternalId,
|
ExternalId,
|
||||||
MIN([ReadOnly]) AS [ReadOnly],
|
MIN([ReadOnly]) AS [ReadOnly],
|
||||||
MIN([HidePasswords]) AS [HidePasswords],
|
MIN([HidePasswords]) AS [HidePasswords],
|
||||||
MIN([Manage]) AS [Manage]
|
MAX([Manage]) AS [Manage]
|
||||||
FROM
|
FROM
|
||||||
[dbo].[UserCollectionDetails](@UserId)
|
[dbo].[UserCollectionDetails](@UserId)
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -13,7 +13,7 @@ BEGIN
|
|||||||
ExternalId,
|
ExternalId,
|
||||||
MIN([ReadOnly]) AS [ReadOnly],
|
MIN([ReadOnly]) AS [ReadOnly],
|
||||||
MIN([HidePasswords]) AS [HidePasswords],
|
MIN([HidePasswords]) AS [HidePasswords],
|
||||||
MIN([Manage]) AS [Manage]
|
MAX([Manage]) AS [Manage]
|
||||||
FROM
|
FROM
|
||||||
[dbo].[UserCollectionDetails_V2](@UserId)
|
[dbo].[UserCollectionDetails_V2](@UserId)
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -13,7 +13,7 @@ BEGIN
|
|||||||
ExternalId,
|
ExternalId,
|
||||||
MIN([ReadOnly]) AS [ReadOnly],
|
MIN([ReadOnly]) AS [ReadOnly],
|
||||||
MIN([HidePasswords]) AS [HidePasswords],
|
MIN([HidePasswords]) AS [HidePasswords],
|
||||||
MIN([Manage]) AS [Manage]
|
MAX([Manage]) AS [Manage]
|
||||||
FROM
|
FROM
|
||||||
[dbo].[UserCollectionDetails](@UserId)
|
[dbo].[UserCollectionDetails](@UserId)
|
||||||
GROUP BY
|
GROUP BY
|
||||||
|
@ -13,7 +13,7 @@ BEGIN
|
|||||||
ExternalId,
|
ExternalId,
|
||||||
MIN([ReadOnly]) AS [ReadOnly],
|
MIN([ReadOnly]) AS [ReadOnly],
|
||||||
MIN([HidePasswords]) AS [HidePasswords],
|
MIN([HidePasswords]) AS [HidePasswords],
|
||||||
MIN([Manage]) AS [Manage]
|
MAX([Manage]) AS [Manage]
|
||||||
FROM
|
FROM
|
||||||
[dbo].[UserCollectionDetails_V2](@UserId)
|
[dbo].[UserCollectionDetails_V2](@UserId)
|
||||||
GROUP BY
|
GROUP BY
|
||||||
|
@ -260,6 +260,54 @@ public class GroupsControllerTests
|
|||||||
Assert.Equal(groupRequestModel.AccessAll, response.AccessAll);
|
Assert.Equal(groupRequestModel.AccessAll, response.AccessAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task Put_UpdateMembers_AdminsCannotAccessAllCollections_ProviderUser_Success(Organization organization, Group group,
|
||||||
|
GroupRequestModel groupRequestModel, List<Guid> currentGroupUsers, Guid savingUserId,
|
||||||
|
SutProvider<GroupsController> sutProvider)
|
||||||
|
{
|
||||||
|
group.OrganizationId = organization.Id;
|
||||||
|
|
||||||
|
// Enable FC and v1
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(
|
||||||
|
new OrganizationAbility
|
||||||
|
{
|
||||||
|
Id = organization.Id,
|
||||||
|
AllowAdminAccessToAllCollectionItems = false,
|
||||||
|
FlexibleCollections = true
|
||||||
|
});
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IGroupRepository>().GetByIdWithCollectionsAsync(group.Id)
|
||||||
|
.Returns(new Tuple<Group, ICollection<CollectionAccessSelection>>(group, new List<CollectionAccessSelection>()));
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ManageGroups(organization.Id).Returns(true);
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetByOrganizationAsync(organization.Id, Arg.Any<Guid>())
|
||||||
|
.Returns((OrganizationUser)null); // Provider is not an OrganizationUser, so it will always return null
|
||||||
|
sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||||
|
.Returns(savingUserId);
|
||||||
|
sutProvider.GetDependency<IGroupRepository>().GetManyUserIdsByIdAsync(group.Id)
|
||||||
|
.Returns(currentGroupUsers);
|
||||||
|
|
||||||
|
// Make collection authorization pass, it's not being tested here
|
||||||
|
groupRequestModel.Collections = Array.Empty<SelectionReadOnlyRequestModel>();
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Put(organization.Id, group.Id, groupRequestModel);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ICurrentContext>().Received(1).ManageGroups(organization.Id);
|
||||||
|
await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(
|
||||||
|
Arg.Is<Group>(g =>
|
||||||
|
g.OrganizationId == organization.Id && g.Name == groupRequestModel.Name &&
|
||||||
|
g.AccessAll == groupRequestModel.AccessAll),
|
||||||
|
Arg.Is<Organization>(o => o.Id == organization.Id),
|
||||||
|
Arg.Any<ICollection<CollectionAccessSelection>>(),
|
||||||
|
Arg.Any<IEnumerable<Guid>>());
|
||||||
|
Assert.Equal(groupRequestModel.Name, response.Name);
|
||||||
|
Assert.Equal(organization.Id, response.OrganizationId);
|
||||||
|
Assert.Equal(groupRequestModel.AccessAll, response.AccessAll);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task Put_UpdateCollections_OnlyUpdatesCollectionsTheSavingUserCanUpdate(GroupRequestModel groupRequestModel,
|
public async Task Put_UpdateCollections_OnlyUpdatesCollectionsTheSavingUserCanUpdate(GroupRequestModel groupRequestModel,
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using AutoFixture.Xunit2;
|
using AutoFixture.Xunit2;
|
||||||
using Bit.Api.AdminConsole.Controllers;
|
using Bit.Api.AdminConsole.Controllers;
|
||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
|
||||||
using Bit.Api.Auth.Models.Request.Accounts;
|
using Bit.Api.Auth.Models.Request.Accounts;
|
||||||
using Bit.Api.Models.Request.Organizations;
|
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
@ -16,21 +15,15 @@ using Bit.Core.Auth.Models.Data;
|
|||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Auth.Services;
|
using Bit.Core.Auth.Services;
|
||||||
using Bit.Core.Billing.Commands;
|
using Bit.Core.Billing.Commands;
|
||||||
using Bit.Core.Billing.Queries;
|
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Business;
|
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tokens;
|
||||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using NSubstitute.ReturnsExtensions;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||||
|
|
||||||
@ -43,7 +36,6 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IPaymentService _paymentService;
|
|
||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyRepository _policyRepository;
|
||||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||||
private readonly ISsoConfigService _ssoConfigService;
|
private readonly ISsoConfigService _ssoConfigService;
|
||||||
@ -51,20 +43,13 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
private readonly IGetOrganizationApiKeyQuery _getOrganizationApiKeyQuery;
|
private readonly IGetOrganizationApiKeyQuery _getOrganizationApiKeyQuery;
|
||||||
private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;
|
private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;
|
||||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||||
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
|
|
||||||
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
|
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly ILicensingService _licensingService;
|
|
||||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
|
||||||
private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand;
|
|
||||||
private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand;
|
|
||||||
private readonly IPushNotificationService _pushNotificationService;
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
private readonly ICancelSubscriptionCommand _cancelSubscriptionCommand;
|
|
||||||
private readonly ISubscriberQueries _subscriberQueries;
|
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
|
||||||
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
|
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
|
||||||
private readonly IProviderRepository _providerRepository;
|
private readonly IProviderRepository _providerRepository;
|
||||||
private readonly IScaleSeatsCommand _scaleSeatsCommand;
|
private readonly IScaleSeatsCommand _scaleSeatsCommand;
|
||||||
|
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
|
||||||
|
|
||||||
private readonly OrganizationsController _sut;
|
private readonly OrganizationsController _sut;
|
||||||
|
|
||||||
@ -75,7 +60,6 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||||
_organizationService = Substitute.For<IOrganizationService>();
|
_organizationService = Substitute.For<IOrganizationService>();
|
||||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||||
_paymentService = Substitute.For<IPaymentService>();
|
|
||||||
_policyRepository = Substitute.For<IPolicyRepository>();
|
_policyRepository = Substitute.For<IPolicyRepository>();
|
||||||
_ssoConfigRepository = Substitute.For<ISsoConfigRepository>();
|
_ssoConfigRepository = Substitute.For<ISsoConfigRepository>();
|
||||||
_ssoConfigService = Substitute.For<ISsoConfigService>();
|
_ssoConfigService = Substitute.For<ISsoConfigService>();
|
||||||
@ -83,20 +67,13 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
_rotateOrganizationApiKeyCommand = Substitute.For<IRotateOrganizationApiKeyCommand>();
|
_rotateOrganizationApiKeyCommand = Substitute.For<IRotateOrganizationApiKeyCommand>();
|
||||||
_organizationApiKeyRepository = Substitute.For<IOrganizationApiKeyRepository>();
|
_organizationApiKeyRepository = Substitute.For<IOrganizationApiKeyRepository>();
|
||||||
_userService = Substitute.For<IUserService>();
|
_userService = Substitute.For<IUserService>();
|
||||||
_cloudGetOrganizationLicenseQuery = Substitute.For<ICloudGetOrganizationLicenseQuery>();
|
|
||||||
_createOrganizationApiKeyCommand = Substitute.For<ICreateOrganizationApiKeyCommand>();
|
_createOrganizationApiKeyCommand = Substitute.For<ICreateOrganizationApiKeyCommand>();
|
||||||
_featureService = Substitute.For<IFeatureService>();
|
_featureService = Substitute.For<IFeatureService>();
|
||||||
_licensingService = Substitute.For<ILicensingService>();
|
|
||||||
_updateSecretsManagerSubscriptionCommand = Substitute.For<IUpdateSecretsManagerSubscriptionCommand>();
|
|
||||||
_upgradeOrganizationPlanCommand = Substitute.For<IUpgradeOrganizationPlanCommand>();
|
|
||||||
_addSecretsManagerSubscriptionCommand = Substitute.For<IAddSecretsManagerSubscriptionCommand>();
|
|
||||||
_pushNotificationService = Substitute.For<IPushNotificationService>();
|
_pushNotificationService = Substitute.For<IPushNotificationService>();
|
||||||
_cancelSubscriptionCommand = Substitute.For<ICancelSubscriptionCommand>();
|
|
||||||
_subscriberQueries = Substitute.For<ISubscriberQueries>();
|
|
||||||
_referenceEventService = Substitute.For<IReferenceEventService>();
|
|
||||||
_organizationEnableCollectionEnhancementsCommand = Substitute.For<IOrganizationEnableCollectionEnhancementsCommand>();
|
_organizationEnableCollectionEnhancementsCommand = Substitute.For<IOrganizationEnableCollectionEnhancementsCommand>();
|
||||||
_providerRepository = Substitute.For<IProviderRepository>();
|
_providerRepository = Substitute.For<IProviderRepository>();
|
||||||
_scaleSeatsCommand = Substitute.For<IScaleSeatsCommand>();
|
_scaleSeatsCommand = Substitute.For<IScaleSeatsCommand>();
|
||||||
|
_orgDeleteTokenDataFactory = Substitute.For<IDataProtectorTokenFactory<OrgDeleteTokenable>>();
|
||||||
|
|
||||||
_sut = new OrganizationsController(
|
_sut = new OrganizationsController(
|
||||||
_organizationRepository,
|
_organizationRepository,
|
||||||
@ -104,7 +81,6 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
_policyRepository,
|
_policyRepository,
|
||||||
_organizationService,
|
_organizationService,
|
||||||
_userService,
|
_userService,
|
||||||
_paymentService,
|
|
||||||
_currentContext,
|
_currentContext,
|
||||||
_ssoConfigRepository,
|
_ssoConfigRepository,
|
||||||
_ssoConfigService,
|
_ssoConfigService,
|
||||||
@ -112,20 +88,13 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
_rotateOrganizationApiKeyCommand,
|
_rotateOrganizationApiKeyCommand,
|
||||||
_createOrganizationApiKeyCommand,
|
_createOrganizationApiKeyCommand,
|
||||||
_organizationApiKeyRepository,
|
_organizationApiKeyRepository,
|
||||||
_cloudGetOrganizationLicenseQuery,
|
|
||||||
_featureService,
|
_featureService,
|
||||||
_globalSettings,
|
_globalSettings,
|
||||||
_licensingService,
|
|
||||||
_updateSecretsManagerSubscriptionCommand,
|
|
||||||
_upgradeOrganizationPlanCommand,
|
|
||||||
_addSecretsManagerSubscriptionCommand,
|
|
||||||
_pushNotificationService,
|
_pushNotificationService,
|
||||||
_cancelSubscriptionCommand,
|
|
||||||
_subscriberQueries,
|
|
||||||
_referenceEventService,
|
|
||||||
_organizationEnableCollectionEnhancementsCommand,
|
_organizationEnableCollectionEnhancementsCommand,
|
||||||
_providerRepository,
|
_providerRepository,
|
||||||
_scaleSeatsCommand);
|
_scaleSeatsCommand,
|
||||||
|
_orgDeleteTokenDataFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@ -193,196 +162,6 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
await _organizationService.Received(1).DeleteUserAsync(orgId, user.Id);
|
await _organizationService.Received(1).DeleteUserAsync(orgId, user.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, AutoData]
|
|
||||||
public async Task OrganizationsController_PostUpgrade_UserCannotEditSubscription_ThrowsNotFoundException(
|
|
||||||
Guid organizationId,
|
|
||||||
OrganizationUpgradeRequestModel model)
|
|
||||||
{
|
|
||||||
_currentContext.EditSubscription(organizationId).Returns(false);
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostUpgrade(organizationId.ToString(), model));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, AutoData]
|
|
||||||
public async Task OrganizationsController_PostUpgrade_NonSMUpgrade_ReturnsCorrectResponse(
|
|
||||||
Guid organizationId,
|
|
||||||
OrganizationUpgradeRequestModel model,
|
|
||||||
bool success,
|
|
||||||
string paymentIntentClientSecret)
|
|
||||||
{
|
|
||||||
model.UseSecretsManager = false;
|
|
||||||
|
|
||||||
_currentContext.EditSubscription(organizationId).Returns(true);
|
|
||||||
|
|
||||||
_upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>())
|
|
||||||
.Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));
|
|
||||||
|
|
||||||
var response = await _sut.PostUpgrade(organizationId.ToString(), model);
|
|
||||||
|
|
||||||
Assert.Equal(success, response.Success);
|
|
||||||
Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, AutoData]
|
|
||||||
public async Task OrganizationsController_PostUpgrade_SMUpgrade_ProvidesAccess_ReturnsCorrectResponse(
|
|
||||||
Guid organizationId,
|
|
||||||
Guid userId,
|
|
||||||
OrganizationUpgradeRequestModel model,
|
|
||||||
bool success,
|
|
||||||
string paymentIntentClientSecret,
|
|
||||||
OrganizationUser organizationUser)
|
|
||||||
{
|
|
||||||
model.UseSecretsManager = true;
|
|
||||||
organizationUser.AccessSecretsManager = false;
|
|
||||||
|
|
||||||
_currentContext.EditSubscription(organizationId).Returns(true);
|
|
||||||
|
|
||||||
_upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>())
|
|
||||||
.Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));
|
|
||||||
|
|
||||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
|
||||||
|
|
||||||
_organizationUserRepository.GetByOrganizationAsync(organizationId, userId).Returns(organizationUser);
|
|
||||||
|
|
||||||
var response = await _sut.PostUpgrade(organizationId.ToString(), model);
|
|
||||||
|
|
||||||
Assert.Equal(success, response.Success);
|
|
||||||
Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);
|
|
||||||
|
|
||||||
await _organizationUserRepository.Received(1).ReplaceAsync(Arg.Is<OrganizationUser>(orgUser =>
|
|
||||||
orgUser.Id == organizationUser.Id && orgUser.AccessSecretsManager == true));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, AutoData]
|
|
||||||
public async Task OrganizationsController_PostUpgrade_SMUpgrade_NullOrgUser_ReturnsCorrectResponse(
|
|
||||||
Guid organizationId,
|
|
||||||
Guid userId,
|
|
||||||
OrganizationUpgradeRequestModel model,
|
|
||||||
bool success,
|
|
||||||
string paymentIntentClientSecret)
|
|
||||||
{
|
|
||||||
model.UseSecretsManager = true;
|
|
||||||
|
|
||||||
_currentContext.EditSubscription(organizationId).Returns(true);
|
|
||||||
|
|
||||||
_upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>())
|
|
||||||
.Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));
|
|
||||||
|
|
||||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
|
||||||
|
|
||||||
_organizationUserRepository.GetByOrganizationAsync(organizationId, userId).ReturnsNull();
|
|
||||||
|
|
||||||
var response = await _sut.PostUpgrade(organizationId.ToString(), model);
|
|
||||||
|
|
||||||
Assert.Equal(success, response.Success);
|
|
||||||
Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);
|
|
||||||
|
|
||||||
await _organizationUserRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<OrganizationUser>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, AutoData]
|
|
||||||
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_NullOrg_ThrowsNotFoundException(
|
|
||||||
Guid organizationId,
|
|
||||||
SecretsManagerSubscribeRequestModel model)
|
|
||||||
{
|
|
||||||
_organizationRepository.GetByIdAsync(organizationId).ReturnsNull();
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostSubscribeSecretsManagerAsync(organizationId, model));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, AutoData]
|
|
||||||
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_UserCannotEditSubscription_ThrowsNotFoundException(
|
|
||||||
Guid organizationId,
|
|
||||||
SecretsManagerSubscribeRequestModel model,
|
|
||||||
Organization organization)
|
|
||||||
{
|
|
||||||
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
|
||||||
|
|
||||||
_currentContext.EditSubscription(organizationId).Returns(false);
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostSubscribeSecretsManagerAsync(organizationId, model));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, AutoData]
|
|
||||||
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_ProvidesAccess_ReturnsCorrectResponse(
|
|
||||||
Guid organizationId,
|
|
||||||
SecretsManagerSubscribeRequestModel model,
|
|
||||||
Organization organization,
|
|
||||||
Guid userId,
|
|
||||||
OrganizationUser organizationUser,
|
|
||||||
OrganizationUserOrganizationDetails organizationUserOrganizationDetails)
|
|
||||||
{
|
|
||||||
organizationUser.AccessSecretsManager = false;
|
|
||||||
|
|
||||||
var ssoConfigurationData = new SsoConfigurationData
|
|
||||||
{
|
|
||||||
MemberDecryptionType = MemberDecryptionType.KeyConnector,
|
|
||||||
KeyConnectorUrl = "https://example.com"
|
|
||||||
};
|
|
||||||
|
|
||||||
organizationUserOrganizationDetails.Permissions = string.Empty;
|
|
||||||
organizationUserOrganizationDetails.SsoConfig = ssoConfigurationData.Serialize();
|
|
||||||
|
|
||||||
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
|
||||||
|
|
||||||
_currentContext.EditSubscription(organizationId).Returns(true);
|
|
||||||
|
|
||||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
|
||||||
|
|
||||||
_organizationUserRepository.GetByOrganizationAsync(organization.Id, userId).Returns(organizationUser);
|
|
||||||
|
|
||||||
_organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed)
|
|
||||||
.Returns(organizationUserOrganizationDetails);
|
|
||||||
|
|
||||||
var response = await _sut.PostSubscribeSecretsManagerAsync(organizationId, model);
|
|
||||||
|
|
||||||
Assert.Equal(response.Id, organizationUserOrganizationDetails.OrganizationId);
|
|
||||||
Assert.Equal(response.Name, organizationUserOrganizationDetails.Name);
|
|
||||||
|
|
||||||
await _addSecretsManagerSubscriptionCommand.Received(1)
|
|
||||||
.SignUpAsync(organization, model.AdditionalSmSeats, model.AdditionalServiceAccounts);
|
|
||||||
await _organizationUserRepository.Received(1).ReplaceAsync(Arg.Is<OrganizationUser>(orgUser =>
|
|
||||||
orgUser.Id == organizationUser.Id && orgUser.AccessSecretsManager == true));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, AutoData]
|
|
||||||
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_NullOrgUser_ReturnsCorrectResponse(
|
|
||||||
Guid organizationId,
|
|
||||||
SecretsManagerSubscribeRequestModel model,
|
|
||||||
Organization organization,
|
|
||||||
Guid userId,
|
|
||||||
OrganizationUserOrganizationDetails organizationUserOrganizationDetails)
|
|
||||||
{
|
|
||||||
var ssoConfigurationData = new SsoConfigurationData
|
|
||||||
{
|
|
||||||
MemberDecryptionType = MemberDecryptionType.KeyConnector,
|
|
||||||
KeyConnectorUrl = "https://example.com"
|
|
||||||
};
|
|
||||||
|
|
||||||
organizationUserOrganizationDetails.Permissions = string.Empty;
|
|
||||||
organizationUserOrganizationDetails.SsoConfig = ssoConfigurationData.Serialize();
|
|
||||||
|
|
||||||
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
|
||||||
|
|
||||||
_currentContext.EditSubscription(organizationId).Returns(true);
|
|
||||||
|
|
||||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
|
||||||
|
|
||||||
_organizationUserRepository.GetByOrganizationAsync(organization.Id, userId).ReturnsNull();
|
|
||||||
|
|
||||||
_organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed)
|
|
||||||
.Returns(organizationUserOrganizationDetails);
|
|
||||||
|
|
||||||
var response = await _sut.PostSubscribeSecretsManagerAsync(organizationId, model);
|
|
||||||
|
|
||||||
Assert.Equal(response.Id, organizationUserOrganizationDetails.OrganizationId);
|
|
||||||
Assert.Equal(response.Name, organizationUserOrganizationDetails.Name);
|
|
||||||
|
|
||||||
await _addSecretsManagerSubscriptionCommand.Received(1)
|
|
||||||
.SignUpAsync(organization, model.AdditionalSmSeats, model.AdditionalServiceAccounts);
|
|
||||||
await _organizationUserRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<OrganizationUser>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, AutoData]
|
[Theory, AutoData]
|
||||||
public async Task EnableCollectionEnhancements_Success(Organization organization)
|
public async Task EnableCollectionEnhancements_Success(Organization organization)
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,317 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using AutoFixture.Xunit2;
|
||||||
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
|
using Bit.Api.Billing.Controllers;
|
||||||
|
using Bit.Api.Models.Request.Organizations;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Auth.Entities;
|
||||||
|
using Bit.Core.Auth.Enums;
|
||||||
|
using Bit.Core.Auth.Models.Data;
|
||||||
|
using Bit.Core.Auth.Repositories;
|
||||||
|
using Bit.Core.Auth.Services;
|
||||||
|
using Bit.Core.Billing.Commands;
|
||||||
|
using Bit.Core.Billing.Queries;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||||
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Tools.Services;
|
||||||
|
using NSubstitute;
|
||||||
|
using NSubstitute.ReturnsExtensions;
|
||||||
|
using Xunit;
|
||||||
|
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||||
|
|
||||||
|
namespace Bit.Api.Test.Billing.Controllers;
|
||||||
|
|
||||||
|
public class OrganizationsControllerTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly IOrganizationService _organizationService;
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
private readonly IPaymentService _paymentService;
|
||||||
|
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
|
||||||
|
private readonly ILicensingService _licensingService;
|
||||||
|
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||||
|
private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand;
|
||||||
|
private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand;
|
||||||
|
private readonly ICancelSubscriptionCommand _cancelSubscriptionCommand;
|
||||||
|
private readonly ISubscriberQueries _subscriberQueries;
|
||||||
|
private readonly IReferenceEventService _referenceEventService;
|
||||||
|
|
||||||
|
private readonly OrganizationsController _sut;
|
||||||
|
|
||||||
|
public OrganizationsControllerTests()
|
||||||
|
{
|
||||||
|
_currentContext = Substitute.For<ICurrentContext>();
|
||||||
|
_globalSettings = Substitute.For<GlobalSettings>();
|
||||||
|
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||||
|
_organizationService = Substitute.For<IOrganizationService>();
|
||||||
|
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||||
|
_paymentService = Substitute.For<IPaymentService>();
|
||||||
|
Substitute.For<IPolicyRepository>();
|
||||||
|
_ssoConfigRepository = Substitute.For<ISsoConfigRepository>();
|
||||||
|
Substitute.For<ISsoConfigService>();
|
||||||
|
_userService = Substitute.For<IUserService>();
|
||||||
|
_cloudGetOrganizationLicenseQuery = Substitute.For<ICloudGetOrganizationLicenseQuery>();
|
||||||
|
_licensingService = Substitute.For<ILicensingService>();
|
||||||
|
_updateSecretsManagerSubscriptionCommand = Substitute.For<IUpdateSecretsManagerSubscriptionCommand>();
|
||||||
|
_upgradeOrganizationPlanCommand = Substitute.For<IUpgradeOrganizationPlanCommand>();
|
||||||
|
_addSecretsManagerSubscriptionCommand = Substitute.For<IAddSecretsManagerSubscriptionCommand>();
|
||||||
|
_cancelSubscriptionCommand = Substitute.For<ICancelSubscriptionCommand>();
|
||||||
|
_subscriberQueries = Substitute.For<ISubscriberQueries>();
|
||||||
|
_referenceEventService = Substitute.For<IReferenceEventService>();
|
||||||
|
|
||||||
|
_sut = new OrganizationsController(
|
||||||
|
_organizationRepository,
|
||||||
|
_organizationUserRepository,
|
||||||
|
_organizationService,
|
||||||
|
_userService,
|
||||||
|
_paymentService,
|
||||||
|
_currentContext,
|
||||||
|
_cloudGetOrganizationLicenseQuery,
|
||||||
|
_globalSettings,
|
||||||
|
_licensingService,
|
||||||
|
_updateSecretsManagerSubscriptionCommand,
|
||||||
|
_upgradeOrganizationPlanCommand,
|
||||||
|
_addSecretsManagerSubscriptionCommand,
|
||||||
|
_cancelSubscriptionCommand,
|
||||||
|
_subscriberQueries,
|
||||||
|
_referenceEventService);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_sut?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineAutoData(true, false)]
|
||||||
|
[InlineAutoData(false, true)]
|
||||||
|
[InlineAutoData(false, false)]
|
||||||
|
public async Task OrganizationsController_UserCanLeaveOrganizationThatDoesntProvideKeyConnector(
|
||||||
|
bool keyConnectorEnabled, bool userUsesKeyConnector, Guid orgId, User user)
|
||||||
|
{
|
||||||
|
var ssoConfig = new SsoConfig
|
||||||
|
{
|
||||||
|
Id = default,
|
||||||
|
Data = new SsoConfigurationData
|
||||||
|
{
|
||||||
|
MemberDecryptionType = keyConnectorEnabled
|
||||||
|
? MemberDecryptionType.KeyConnector
|
||||||
|
: MemberDecryptionType.MasterPassword
|
||||||
|
}.Serialize(),
|
||||||
|
Enabled = true,
|
||||||
|
OrganizationId = orgId,
|
||||||
|
};
|
||||||
|
|
||||||
|
user.UsesKeyConnector = userUsesKeyConnector;
|
||||||
|
|
||||||
|
_currentContext.OrganizationUser(orgId).Returns(true);
|
||||||
|
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
|
||||||
|
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||||
|
|
||||||
|
await _organizationService.DeleteUserAsync(orgId, user.Id);
|
||||||
|
await _organizationService.Received(1).DeleteUserAsync(orgId, user.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostUpgrade_UserCannotEditSubscription_ThrowsNotFoundException(
|
||||||
|
Guid organizationId,
|
||||||
|
OrganizationUpgradeRequestModel model)
|
||||||
|
{
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(false);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostUpgrade(organizationId, model));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostUpgrade_NonSMUpgrade_ReturnsCorrectResponse(
|
||||||
|
Guid organizationId,
|
||||||
|
OrganizationUpgradeRequestModel model,
|
||||||
|
bool success,
|
||||||
|
string paymentIntentClientSecret)
|
||||||
|
{
|
||||||
|
model.UseSecretsManager = false;
|
||||||
|
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||||
|
|
||||||
|
_upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>())
|
||||||
|
.Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));
|
||||||
|
|
||||||
|
var response = await _sut.PostUpgrade(organizationId, model);
|
||||||
|
|
||||||
|
Assert.Equal(success, response.Success);
|
||||||
|
Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostUpgrade_SMUpgrade_ProvidesAccess_ReturnsCorrectResponse(
|
||||||
|
Guid organizationId,
|
||||||
|
Guid userId,
|
||||||
|
OrganizationUpgradeRequestModel model,
|
||||||
|
bool success,
|
||||||
|
string paymentIntentClientSecret,
|
||||||
|
OrganizationUser organizationUser)
|
||||||
|
{
|
||||||
|
model.UseSecretsManager = true;
|
||||||
|
organizationUser.AccessSecretsManager = false;
|
||||||
|
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||||
|
|
||||||
|
_upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>())
|
||||||
|
.Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));
|
||||||
|
|
||||||
|
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||||
|
|
||||||
|
_organizationUserRepository.GetByOrganizationAsync(organizationId, userId).Returns(organizationUser);
|
||||||
|
|
||||||
|
var response = await _sut.PostUpgrade(organizationId, model);
|
||||||
|
|
||||||
|
Assert.Equal(success, response.Success);
|
||||||
|
Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);
|
||||||
|
|
||||||
|
await _organizationUserRepository.Received(1).ReplaceAsync(Arg.Is<OrganizationUser>(orgUser =>
|
||||||
|
orgUser.Id == organizationUser.Id && orgUser.AccessSecretsManager == true));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostUpgrade_SMUpgrade_NullOrgUser_ReturnsCorrectResponse(
|
||||||
|
Guid organizationId,
|
||||||
|
Guid userId,
|
||||||
|
OrganizationUpgradeRequestModel model,
|
||||||
|
bool success,
|
||||||
|
string paymentIntentClientSecret)
|
||||||
|
{
|
||||||
|
model.UseSecretsManager = true;
|
||||||
|
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||||
|
|
||||||
|
_upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>())
|
||||||
|
.Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));
|
||||||
|
|
||||||
|
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||||
|
|
||||||
|
_organizationUserRepository.GetByOrganizationAsync(organizationId, userId).ReturnsNull();
|
||||||
|
|
||||||
|
var response = await _sut.PostUpgrade(organizationId, model);
|
||||||
|
|
||||||
|
Assert.Equal(success, response.Success);
|
||||||
|
Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);
|
||||||
|
|
||||||
|
await _organizationUserRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<OrganizationUser>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_NullOrg_ThrowsNotFoundException(
|
||||||
|
Guid organizationId,
|
||||||
|
SecretsManagerSubscribeRequestModel model)
|
||||||
|
{
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).ReturnsNull();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostSubscribeSecretsManagerAsync(organizationId, model));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_UserCannotEditSubscription_ThrowsNotFoundException(
|
||||||
|
Guid organizationId,
|
||||||
|
SecretsManagerSubscribeRequestModel model,
|
||||||
|
Organization organization)
|
||||||
|
{
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(false);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostSubscribeSecretsManagerAsync(organizationId, model));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_ProvidesAccess_ReturnsCorrectResponse(
|
||||||
|
Guid organizationId,
|
||||||
|
SecretsManagerSubscribeRequestModel model,
|
||||||
|
Organization organization,
|
||||||
|
Guid userId,
|
||||||
|
OrganizationUser organizationUser,
|
||||||
|
OrganizationUserOrganizationDetails organizationUserOrganizationDetails)
|
||||||
|
{
|
||||||
|
organizationUser.AccessSecretsManager = false;
|
||||||
|
|
||||||
|
var ssoConfigurationData = new SsoConfigurationData
|
||||||
|
{
|
||||||
|
MemberDecryptionType = MemberDecryptionType.KeyConnector,
|
||||||
|
KeyConnectorUrl = "https://example.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
organizationUserOrganizationDetails.Permissions = string.Empty;
|
||||||
|
organizationUserOrganizationDetails.SsoConfig = ssoConfigurationData.Serialize();
|
||||||
|
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||||
|
|
||||||
|
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||||
|
|
||||||
|
_organizationUserRepository.GetByOrganizationAsync(organization.Id, userId).Returns(organizationUser);
|
||||||
|
|
||||||
|
_organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed)
|
||||||
|
.Returns(organizationUserOrganizationDetails);
|
||||||
|
|
||||||
|
var response = await _sut.PostSubscribeSecretsManagerAsync(organizationId, model);
|
||||||
|
|
||||||
|
Assert.Equal(response.Id, organizationUserOrganizationDetails.OrganizationId);
|
||||||
|
Assert.Equal(response.Name, organizationUserOrganizationDetails.Name);
|
||||||
|
|
||||||
|
await _addSecretsManagerSubscriptionCommand.Received(1)
|
||||||
|
.SignUpAsync(organization, model.AdditionalSmSeats, model.AdditionalServiceAccounts);
|
||||||
|
await _organizationUserRepository.Received(1).ReplaceAsync(Arg.Is<OrganizationUser>(orgUser =>
|
||||||
|
orgUser.Id == organizationUser.Id && orgUser.AccessSecretsManager == true));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_NullOrgUser_ReturnsCorrectResponse(
|
||||||
|
Guid organizationId,
|
||||||
|
SecretsManagerSubscribeRequestModel model,
|
||||||
|
Organization organization,
|
||||||
|
Guid userId,
|
||||||
|
OrganizationUserOrganizationDetails organizationUserOrganizationDetails)
|
||||||
|
{
|
||||||
|
var ssoConfigurationData = new SsoConfigurationData
|
||||||
|
{
|
||||||
|
MemberDecryptionType = MemberDecryptionType.KeyConnector,
|
||||||
|
KeyConnectorUrl = "https://example.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
organizationUserOrganizationDetails.Permissions = string.Empty;
|
||||||
|
organizationUserOrganizationDetails.SsoConfig = ssoConfigurationData.Serialize();
|
||||||
|
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||||
|
|
||||||
|
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||||
|
|
||||||
|
_organizationUserRepository.GetByOrganizationAsync(organization.Id, userId).ReturnsNull();
|
||||||
|
|
||||||
|
_organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed)
|
||||||
|
.Returns(organizationUserOrganizationDetails);
|
||||||
|
|
||||||
|
var response = await _sut.PostSubscribeSecretsManagerAsync(organizationId, model);
|
||||||
|
|
||||||
|
Assert.Equal(response.Id, organizationUserOrganizationDetails.OrganizationId);
|
||||||
|
Assert.Equal(response.Name, organizationUserOrganizationDetails.Name);
|
||||||
|
|
||||||
|
await _addSecretsManagerSubscriptionCommand.Received(1)
|
||||||
|
.SignUpAsync(organization, model.AdditionalSmSeats, model.AdditionalServiceAccounts);
|
||||||
|
await _organizationUserRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<OrganizationUser>());
|
||||||
|
}
|
||||||
|
}
|
@ -301,7 +301,7 @@ public class ProviderClientsControllerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task UpdateAsync_NoContent(
|
public async Task UpdateAsync_AssignedSeats_NoContent(
|
||||||
Guid providerId,
|
Guid providerId,
|
||||||
Guid providerOrganizationId,
|
Guid providerOrganizationId,
|
||||||
UpdateClientOrganizationRequestBody requestBody,
|
UpdateClientOrganizationRequestBody requestBody,
|
||||||
@ -333,6 +333,50 @@ public class ProviderClientsControllerTests
|
|||||||
organization,
|
organization,
|
||||||
requestBody.AssignedSeats);
|
requestBody.AssignedSeats);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>().Received(1)
|
||||||
|
.ReplaceAsync(Arg.Is<Organization>(org => org.Name == requestBody.Name));
|
||||||
|
|
||||||
|
Assert.IsType<Ok>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task UpdateAsync_Name_NoContent(
|
||||||
|
Guid providerId,
|
||||||
|
Guid providerOrganizationId,
|
||||||
|
UpdateClientOrganizationRequestBody requestBody,
|
||||||
|
Provider provider,
|
||||||
|
ProviderOrganization providerOrganization,
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<ProviderClientsController> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(providerId)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId)
|
||||||
|
.Returns(provider);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganizationId)
|
||||||
|
.Returns(providerOrganization);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(providerOrganization.OrganizationId)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
requestBody.AssignedSeats = organization.Seats!.Value;
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.UpdateAsync(providerId, providerOrganizationId, requestBody);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IAssignSeatsToClientOrganizationCommand>().DidNotReceiveWithAnyArgs()
|
||||||
|
.AssignSeatsToClientOrganization(
|
||||||
|
Arg.Any<Provider>(),
|
||||||
|
Arg.Any<Organization>(),
|
||||||
|
Arg.Any<int>());
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>().Received(1)
|
||||||
|
.ReplaceAsync(Arg.Is<Organization>(org => org.Name == requestBody.Name));
|
||||||
|
|
||||||
Assert.IsType<Ok>(result);
|
Assert.IsType<Ok>(result);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -828,108 +828,169 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData, CollectionCustomization]
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
public async Task CanUpdateUsers_WithManageUsersCustomPermission_Success(
|
public async Task CanUpdateUsers_WithManageUsersCustomPermission_V1Disabled_Success(
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, ICollection<Collection> collections,
|
||||||
ICollection<Collection> collections,
|
CurrentContextOrganization organization, Guid actingUserId)
|
||||||
CurrentContextOrganization organization)
|
|
||||||
{
|
{
|
||||||
var actingUserId = Guid.NewGuid();
|
|
||||||
|
|
||||||
organization.Type = OrganizationUserType.Custom;
|
organization.Type = OrganizationUserType.Custom;
|
||||||
organization.Permissions = new Permissions
|
organization.Permissions = new Permissions
|
||||||
{
|
{
|
||||||
ManageUsers = true
|
ManageUsers = true
|
||||||
};
|
};
|
||||||
|
|
||||||
var operationsToTest = new[]
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
{
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
BulkCollectionOperations.ModifyUserAccess,
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1)
|
||||||
};
|
.Returns(false);
|
||||||
|
|
||||||
foreach (var op in operationsToTest)
|
var context = new AuthorizationHandlerContext(
|
||||||
{
|
new[] { BulkCollectionOperations.ModifyUserAccess },
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
new ClaimsPrincipal(),
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
collections);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
new[] { op },
|
|
||||||
new ClaimsPrincipal(),
|
|
||||||
collections);
|
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
Assert.True(context.HasSucceeded);
|
||||||
|
|
||||||
Assert.True(context.HasSucceeded);
|
|
||||||
|
|
||||||
// Recreate the SUT to reset the mocks/dependencies between tests
|
|
||||||
sutProvider.Recreate();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData, CollectionCustomization]
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
public async Task CanUpdateGroups_WithManageGroupsCustomPermission_Success(
|
public async Task CanUpdateUsers_WithManageUsersCustomPermission_AllowAdminAccessIsTrue_Success(
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, ICollection<Collection> collections,
|
||||||
ICollection<Collection> collections,
|
CurrentContextOrganization organization, Guid actingUserId)
|
||||||
CurrentContextOrganization organization)
|
|
||||||
{
|
{
|
||||||
var actingUserId = Guid.NewGuid();
|
organization.Type = OrganizationUserType.Custom;
|
||||||
|
organization.Permissions = new Permissions
|
||||||
|
{
|
||||||
|
ManageUsers = true
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1)
|
||||||
|
.Returns(true);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id)
|
||||||
|
.Returns(new OrganizationAbility { AllowAdminAccessToAllCollectionItems = true });
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.ModifyUserAccess },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
Assert.True(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
|
public async Task CanUpdateUsers_WithManageUsersCustomPermission_AllowAdminAccessIsFalse_Failure(
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, ICollection<Collection> collections,
|
||||||
|
CurrentContextOrganization organization, Guid actingUserId)
|
||||||
|
{
|
||||||
|
organization.Type = OrganizationUserType.Custom;
|
||||||
|
organization.Permissions = new Permissions
|
||||||
|
{
|
||||||
|
ManageUsers = true
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1)
|
||||||
|
.Returns(true);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id)
|
||||||
|
.Returns(new OrganizationAbility { AllowAdminAccessToAllCollectionItems = false });
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.ModifyUserAccess },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
Assert.False(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
|
public async Task CanUpdateGroups_WithManageGroupsCustomPermission_V1Disabled_Success(
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, ICollection<Collection> collections,
|
||||||
|
CurrentContextOrganization organization, Guid actingUserId)
|
||||||
|
{
|
||||||
organization.Type = OrganizationUserType.Custom;
|
organization.Type = OrganizationUserType.Custom;
|
||||||
organization.Permissions = new Permissions
|
organization.Permissions = new Permissions
|
||||||
{
|
{
|
||||||
ManageGroups = true
|
ManageGroups = true
|
||||||
};
|
};
|
||||||
|
|
||||||
var operationsToTest = new[]
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
{
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
BulkCollectionOperations.ModifyGroupAccess,
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1)
|
||||||
};
|
.Returns(false);
|
||||||
|
|
||||||
foreach (var op in operationsToTest)
|
|
||||||
{
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
|
||||||
new[] { op },
|
|
||||||
new ClaimsPrincipal(),
|
|
||||||
collections);
|
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
|
||||||
|
|
||||||
Assert.True(context.HasSucceeded);
|
|
||||||
|
|
||||||
// Recreate the SUT to reset the mocks/dependencies between tests
|
|
||||||
sutProvider.Recreate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, CollectionCustomization]
|
|
||||||
[BitAutoData(OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(OrganizationUserType.Owner)]
|
|
||||||
public async Task CanDeleteAsync_WhenAdminOrOwner_Success(
|
|
||||||
OrganizationUserType userType,
|
|
||||||
Guid userId, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
|
||||||
ICollection<Collection> collections,
|
|
||||||
CurrentContextOrganization organization)
|
|
||||||
{
|
|
||||||
organization.Type = userType;
|
|
||||||
organization.Permissions = new Permissions();
|
|
||||||
|
|
||||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { BulkCollectionOperations.Delete },
|
new[] { BulkCollectionOperations.ModifyGroupAccess },
|
||||||
new ClaimsPrincipal(),
|
new ClaimsPrincipal(),
|
||||||
collections);
|
collections);
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
Assert.True(context.HasSucceeded);
|
Assert.True(context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
|
public async Task CanUpdateGroups_WithManageGroupsCustomPermission_AllowAdminAccessIsTrue_Success(
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, ICollection<Collection> collections,
|
||||||
|
CurrentContextOrganization organization, Guid actingUserId)
|
||||||
|
{
|
||||||
|
organization.Type = OrganizationUserType.Custom;
|
||||||
|
organization.Permissions = new Permissions
|
||||||
|
{
|
||||||
|
ManageGroups = true
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1)
|
||||||
|
.Returns(true);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id)
|
||||||
|
.Returns(new OrganizationAbility { AllowAdminAccessToAllCollectionItems = true });
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.ModifyGroupAccess },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
Assert.True(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
|
public async Task CanUpdateGroups_WithManageGroupsCustomPermission_AllowAdminAccessIsFalse_Failure(
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, ICollection<Collection> collections,
|
||||||
|
CurrentContextOrganization organization, Guid actingUserId)
|
||||||
|
{
|
||||||
|
organization.Type = OrganizationUserType.Custom;
|
||||||
|
organization.Permissions = new Permissions
|
||||||
|
{
|
||||||
|
ManageGroups = true
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1)
|
||||||
|
.Returns(true);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id)
|
||||||
|
.Returns(new OrganizationAbility { AllowAdminAccessToAllCollectionItems = false });
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.ModifyGroupAccess },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
Assert.False(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData, CollectionCustomization]
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
public async Task CanDeleteAsync_WithDeleteAnyCollectionPermission_Success(
|
public async Task CanDeleteAsync_WithDeleteAnyCollectionPermission_Success(
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
@ -959,8 +1020,63 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
Assert.True(context.HasSucceeded);
|
Assert.True(context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, CollectionCustomization]
|
||||||
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
|
public async Task CanDeleteAsync_WhenAdminOrOwner_AllowAdminAccessToAllCollectionItemsTrue_Success(
|
||||||
|
OrganizationUserType userType,
|
||||||
|
Guid userId, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
|
ICollection<Collection> collections,
|
||||||
|
CurrentContextOrganization organization)
|
||||||
|
{
|
||||||
|
organization.Type = userType;
|
||||||
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
ArrangeOrganizationAbility(sutProvider, organization, true);
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.Delete },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
Assert.True(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, CollectionCustomization]
|
||||||
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
|
public async Task CanDeleteAsync_WhenAdminOrOwner_V1FlagDisabled_Success(
|
||||||
|
OrganizationUserType userType,
|
||||||
|
Guid userId, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
|
ICollection<Collection> collections,
|
||||||
|
CurrentContextOrganization organization)
|
||||||
|
{
|
||||||
|
organization.Type = userType;
|
||||||
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
ArrangeOrganizationAbility(sutProvider, organization, true, false);
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.Delete },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(false);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
Assert.True(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData, CollectionCustomization]
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
public async Task CanDeleteAsync_WithManageCollectionPermission_Success(
|
public async Task CanDeleteAsync_WhenUser_LimitCollectionCreationDeletionFalse_WithCanManagePermission_Success(
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
ICollection<CollectionDetails> collections,
|
ICollection<CollectionDetails> collections,
|
||||||
CurrentContextOrganization organization)
|
CurrentContextOrganization organization)
|
||||||
@ -991,6 +1107,184 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
Assert.True(context.HasSucceeded);
|
Assert.True(context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, CollectionCustomization]
|
||||||
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
|
[BitAutoData(OrganizationUserType.User)]
|
||||||
|
public async Task CanDeleteAsync_LimitCollectionCreationDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_Success(
|
||||||
|
OrganizationUserType userType,
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
|
ICollection<CollectionDetails> collections,
|
||||||
|
CurrentContextOrganization organization)
|
||||||
|
{
|
||||||
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
organization.Type = userType;
|
||||||
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
ArrangeOrganizationAbility(sutProvider, organization, false, false);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId, Arg.Any<bool>()).Returns(collections);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true);
|
||||||
|
|
||||||
|
foreach (var c in collections)
|
||||||
|
{
|
||||||
|
c.Manage = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.Delete },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
Assert.True(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, CollectionCustomization]
|
||||||
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
|
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_Success(
|
||||||
|
OrganizationUserType userType,
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
|
ICollection<CollectionDetails> collections,
|
||||||
|
CurrentContextOrganization organization)
|
||||||
|
{
|
||||||
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
organization.Type = userType;
|
||||||
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
ArrangeOrganizationAbility(sutProvider, organization, true, false);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId, Arg.Any<bool>()).Returns(collections);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true);
|
||||||
|
|
||||||
|
foreach (var c in collections)
|
||||||
|
{
|
||||||
|
c.Manage = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.Delete },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
Assert.True(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, CollectionCustomization]
|
||||||
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
|
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_Failure(
|
||||||
|
OrganizationUserType userType,
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
|
ICollection<CollectionDetails> collections,
|
||||||
|
CurrentContextOrganization organization)
|
||||||
|
{
|
||||||
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
organization.Type = userType;
|
||||||
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
ArrangeOrganizationAbility(sutProvider, organization, true, false);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId, Arg.Any<bool>()).Returns(collections);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
|
||||||
|
foreach (var c in collections)
|
||||||
|
{
|
||||||
|
c.Manage = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.Delete },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
Assert.False(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
|
public async Task CanDeleteAsync_WhenUser_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_Failure(
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
|
ICollection<CollectionDetails> collections,
|
||||||
|
CurrentContextOrganization organization)
|
||||||
|
{
|
||||||
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
organization.Type = OrganizationUserType.User;
|
||||||
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
ArrangeOrganizationAbility(sutProvider, organization, true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId, Arg.Any<bool>()).Returns(collections);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
|
||||||
|
foreach (var c in collections)
|
||||||
|
{
|
||||||
|
c.Manage = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.Delete },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
Assert.False(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
|
public async Task CanDeleteAsync_WhenUser_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_Failure(
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
|
ICollection<CollectionDetails> collections,
|
||||||
|
CurrentContextOrganization organization)
|
||||||
|
{
|
||||||
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
organization.Type = OrganizationUserType.User;
|
||||||
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
ArrangeOrganizationAbility(sutProvider, organization, true, false);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId, Arg.Any<bool>()).Returns(collections);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
|
||||||
|
foreach (var c in collections)
|
||||||
|
{
|
||||||
|
c.Manage = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.Delete },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
Assert.False(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, CollectionCustomization]
|
[Theory, CollectionCustomization]
|
||||||
[BitAutoData(OrganizationUserType.User)]
|
[BitAutoData(OrganizationUserType.User)]
|
||||||
[BitAutoData(OrganizationUserType.Custom)]
|
[BitAutoData(OrganizationUserType.Custom)]
|
||||||
@ -1102,7 +1396,8 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
{ collections.First().OrganizationId,
|
{ collections.First().OrganizationId,
|
||||||
new OrganizationAbility
|
new OrganizationAbility
|
||||||
{
|
{
|
||||||
LimitCollectionCreationDeletion = true
|
LimitCollectionCreationDeletion = true,
|
||||||
|
AllowAdminAccessToAllCollectionItems = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1177,12 +1472,14 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
private static void ArrangeOrganizationAbility(
|
private static void ArrangeOrganizationAbility(
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
CurrentContextOrganization organization, bool limitCollectionCreationDeletion)
|
CurrentContextOrganization organization, bool limitCollectionCreationDeletion,
|
||||||
|
bool allowAdminAccessToAllCollectionItems = true)
|
||||||
{
|
{
|
||||||
var organizationAbility = new OrganizationAbility();
|
var organizationAbility = new OrganizationAbility();
|
||||||
organizationAbility.Id = organization.Id;
|
organizationAbility.Id = organization.Id;
|
||||||
organizationAbility.FlexibleCollections = true;
|
organizationAbility.FlexibleCollections = true;
|
||||||
organizationAbility.LimitCollectionCreationDeletion = limitCollectionCreationDeletion;
|
organizationAbility.LimitCollectionCreationDeletion = limitCollectionCreationDeletion;
|
||||||
|
organizationAbility.AllowAdminAccessToAllCollectionItems = allowAdminAccessToAllCollectionItems;
|
||||||
|
|
||||||
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
|
||||||
.Returns(organizationAbility);
|
.Returns(organizationAbility);
|
||||||
|
@ -3,9 +3,11 @@ using Bit.Api.Vault.Controllers;
|
|||||||
using Bit.Api.Vault.Models.Request;
|
using Bit.Api.Vault.Models.Request;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Bit.Core.Vault.Models.Data;
|
using Bit.Core.Vault.Models.Data;
|
||||||
@ -14,7 +16,9 @@ using Bit.Core.Vault.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.ReturnsExtensions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using CipherType = Bit.Core.Vault.Enums.CipherType;
|
||||||
|
|
||||||
namespace Bit.Api.Test.Controllers;
|
namespace Bit.Api.Test.Controllers;
|
||||||
|
|
||||||
@ -50,6 +54,92 @@ public class CiphersControllerTests
|
|||||||
Assert.Equal(isFavorite, result.Favorite);
|
Assert.Equal(isFavorite, result.Favorite);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task PutCollections_vNextShouldThrowExceptionWhenCipherIsNullOrNoOrgValue(Guid id, CipherCollectionsRequestModel model, Guid userId,
|
||||||
|
SutProvider<CiphersController> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IUserService>().GetProperUserId(default).Returns(userId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(Guid.NewGuid()).Returns(false);
|
||||||
|
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, userId, true).ReturnsNull();
|
||||||
|
|
||||||
|
var requestAction = async () => await sutProvider.Sut.PutCollections_vNext(id, model);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(requestAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task PutCollections_vNextShouldSaveUpdatedCipher(Guid id, CipherCollectionsRequestModel model, Guid userId, SutProvider<CiphersController> sutProvider)
|
||||||
|
{
|
||||||
|
SetupUserAndOrgMocks(id, userId, sutProvider);
|
||||||
|
var cipherDetails = CreateCipherDetailsMock(id, userId);
|
||||||
|
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, userId, true).ReturnsForAnyArgs(cipherDetails);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByUserIdCipherIdAsync(userId, id, Arg.Any<bool>()).Returns((ICollection<CollectionCipher>)new List<CollectionCipher>());
|
||||||
|
var cipherService = sutProvider.GetDependency<ICipherService>();
|
||||||
|
|
||||||
|
await sutProvider.Sut.PutCollections_vNext(id, model);
|
||||||
|
|
||||||
|
await cipherService.ReceivedWithAnyArgs().SaveCollectionsAsync(default, default, default, default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task PutCollections_vNextReturnOptionalDetailsCipherUnavailableFalse(Guid id, CipherCollectionsRequestModel model, Guid userId, SutProvider<CiphersController> sutProvider)
|
||||||
|
{
|
||||||
|
SetupUserAndOrgMocks(id, userId, sutProvider);
|
||||||
|
var cipherDetails = CreateCipherDetailsMock(id, userId);
|
||||||
|
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, userId, true).ReturnsForAnyArgs(cipherDetails);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByUserIdCipherIdAsync(userId, id, Arg.Any<bool>()).Returns((ICollection<CollectionCipher>)new List<CollectionCipher>());
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.PutCollections_vNext(id, model);
|
||||||
|
|
||||||
|
Assert.IsType<OptionalCipherDetailsResponseModel>(result);
|
||||||
|
Assert.False(result.Unavailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task PutCollections_vNextReturnOptionalDetailsCipherUnavailableTrue(Guid id, CipherCollectionsRequestModel model, Guid userId, SutProvider<CiphersController> sutProvider)
|
||||||
|
{
|
||||||
|
SetupUserAndOrgMocks(id, userId, sutProvider);
|
||||||
|
var cipherDetails = CreateCipherDetailsMock(id, userId);
|
||||||
|
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, userId, true).ReturnsForAnyArgs(cipherDetails, [(CipherDetails)null]);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByUserIdCipherIdAsync(userId, id, Arg.Any<bool>()).Returns((ICollection<CollectionCipher>)new List<CollectionCipher>());
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.PutCollections_vNext(id, model);
|
||||||
|
|
||||||
|
Assert.IsType<OptionalCipherDetailsResponseModel>(result);
|
||||||
|
Assert.True(result.Unavailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupUserAndOrgMocks(Guid id, Guid userId, SutProvider<CiphersController> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(default).ReturnsForAnyArgs(true);
|
||||||
|
sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByUserIdCipherIdAsync(userId, id, Arg.Any<bool>()).Returns(new List<CollectionCipher>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private CipherDetails CreateCipherDetailsMock(Guid id, Guid userId)
|
||||||
|
{
|
||||||
|
return new CipherDetails
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
UserId = userId,
|
||||||
|
OrganizationId = Guid.NewGuid(),
|
||||||
|
Type = CipherType.Login,
|
||||||
|
Data = @"
|
||||||
|
{
|
||||||
|
""Uris"": [
|
||||||
|
{
|
||||||
|
""Uri"": ""https://bitwarden.com""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
""Username"": ""testuser"",
|
||||||
|
""Password"": ""securepassword123""
|
||||||
|
}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData(OrganizationUserType.Admin, true, true)]
|
[BitAutoData(OrganizationUserType.Admin, true, true)]
|
||||||
[BitAutoData(OrganizationUserType.Owner, true, true)]
|
[BitAutoData(OrganizationUserType.Owner, true, true)]
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using AutoFixture;
|
||||||
|
using AutoFixture.Xunit2;
|
||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.AdminConsole.AutoFixture;
|
||||||
|
|
||||||
|
internal class OrganizationUserPolicyDetailsCustomization : ICustomization
|
||||||
|
{
|
||||||
|
public PolicyType Type { get; set; }
|
||||||
|
|
||||||
|
public OrganizationUserPolicyDetailsCustomization(PolicyType type)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Customize(IFixture fixture)
|
||||||
|
{
|
||||||
|
fixture.Customize<OrganizationUserPolicyDetails>(composer => composer
|
||||||
|
.With(o => o.OrganizationId, Guid.NewGuid())
|
||||||
|
.With(o => o.PolicyType, Type)
|
||||||
|
.With(o => o.PolicyEnabled, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrganizationUserPolicyDetailsAttribute : CustomizeAttribute
|
||||||
|
{
|
||||||
|
private readonly PolicyType _type;
|
||||||
|
|
||||||
|
public OrganizationUserPolicyDetailsAttribute(PolicyType type)
|
||||||
|
{
|
||||||
|
_type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ICustomization GetCustomization(ParameterInfo parameter)
|
||||||
|
{
|
||||||
|
return new OrganizationUserPolicyDetailsCustomization(_type);
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Enums;
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
@ -39,7 +40,6 @@ using NSubstitute.ReturnsExtensions;
|
|||||||
using Xunit;
|
using Xunit;
|
||||||
using Organization = Bit.Core.AdminConsole.Entities.Organization;
|
using Organization = Bit.Core.AdminConsole.Entities.Organization;
|
||||||
using OrganizationUser = Bit.Core.Entities.OrganizationUser;
|
using OrganizationUser = Bit.Core.Entities.OrganizationUser;
|
||||||
using Policy = Bit.Core.AdminConsole.Entities.Policy;
|
|
||||||
|
|
||||||
namespace Bit.Core.Test.Services;
|
namespace Bit.Core.Test.Services;
|
||||||
|
|
||||||
@ -252,7 +252,7 @@ public class OrganizationServiceTests
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData(PlanType.FamiliesAnnually)]
|
[BitAutoData(PlanType.FamiliesAnnually)]
|
||||||
public async Task SignUp_WithFlexibleCollections_SetsAccessAllToFalse
|
public async Task SignUp_EnablesFlexibleCollectionsFeatures
|
||||||
(PlanType planType, OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
|
(PlanType planType, OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
signup.Plan = planType;
|
signup.Plan = planType;
|
||||||
@ -261,10 +261,6 @@ public class OrganizationServiceTests
|
|||||||
signup.PremiumAccessAddon = false;
|
signup.PremiumAccessAddon = false;
|
||||||
signup.UseSecretsManager = false;
|
signup.UseSecretsManager = false;
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.FlexibleCollectionsSignup)
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
// Extract orgUserId when created
|
// Extract orgUserId when created
|
||||||
Guid? orgUserId = null;
|
Guid? orgUserId = null;
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
@ -272,6 +268,10 @@ public class OrganizationServiceTests
|
|||||||
|
|
||||||
var result = await sutProvider.Sut.SignUpAsync(signup);
|
var result = await sutProvider.Sut.SignUpAsync(signup);
|
||||||
|
|
||||||
|
// Assert: Organization.FlexibleCollections is enabled
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>().Received(1)
|
||||||
|
.CreateAsync(Arg.Is<Organization>(o => o.FlexibleCollections));
|
||||||
|
|
||||||
// Assert: AccessAll is not used
|
// Assert: AccessAll is not used
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).CreateAsync(
|
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).CreateAsync(
|
||||||
Arg.Is<OrganizationUser>(o =>
|
Arg.Is<OrganizationUser>(o =>
|
||||||
@ -295,33 +295,6 @@ public class OrganizationServiceTests
|
|||||||
Assert.NotNull(result.Item2);
|
Assert.NotNull(result.Item2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData(PlanType.FamiliesAnnually)]
|
|
||||||
public async Task SignUp_WithoutFlexibleCollections_SetsAccessAllToTrue
|
|
||||||
(PlanType planType, OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
signup.Plan = planType;
|
|
||||||
var plan = StaticStore.GetPlan(signup.Plan);
|
|
||||||
signup.AdditionalSeats = 0;
|
|
||||||
signup.PaymentMethodType = PaymentMethodType.Card;
|
|
||||||
signup.PremiumAccessAddon = false;
|
|
||||||
signup.UseSecretsManager = false;
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.FlexibleCollectionsSignup)
|
|
||||||
.Returns(false);
|
|
||||||
|
|
||||||
var result = await sutProvider.Sut.SignUpAsync(signup);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).CreateAsync(
|
|
||||||
Arg.Is<OrganizationUser>(o =>
|
|
||||||
o.UserId == signup.Owner.Id &&
|
|
||||||
o.AccessAll == true));
|
|
||||||
|
|
||||||
Assert.NotNull(result.Item1);
|
|
||||||
Assert.NotNull(result.Item2);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData(PlanType.EnterpriseAnnually)]
|
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||||
[BitAutoData(PlanType.EnterpriseMonthly)]
|
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||||
@ -1441,15 +1414,15 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task ConfirmUser_SingleOrgPolicy(Organization org, OrganizationUser confirmingUser,
|
public async Task ConfirmUser_AsUser_SingleOrgPolicy_AppliedFromConfirmingOrg_Throws(Organization org, OrganizationUser confirmingUser,
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||||
OrganizationUser orgUserAnotherOrg, [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
|
OrganizationUser orgUserAnotherOrg, [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy,
|
||||||
string key, SutProvider<OrganizationService> sutProvider)
|
string key, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
|
||||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||||
var userService = Substitute.For<IUserService>();
|
var userService = Substitute.For<IUserService>();
|
||||||
|
|
||||||
org.PlanType = PlanType.EnterpriseAnnually;
|
org.PlanType = PlanType.EnterpriseAnnually;
|
||||||
@ -1460,23 +1433,84 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
||||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||||
policyRepository.GetManyByOrganizationIdAsync(org.Id).Returns(new[] { singleOrgPolicy });
|
singleOrgPolicy.OrganizationId = org.Id;
|
||||||
|
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(new[] { singleOrgPolicy });
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService));
|
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService));
|
||||||
Assert.Contains("User is a member of another organization.", exception.Message);
|
Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task ConfirmUser_TwoFactorPolicy(Organization org, OrganizationUser confirmingUser,
|
public async Task ConfirmUser_AsUser_SingleOrgPolicy_AppliedFromOtherOrg_Throws(Organization org, OrganizationUser confirmingUser,
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||||
OrganizationUser orgUserAnotherOrg, [Policy(PolicyType.TwoFactorAuthentication)] Policy twoFactorPolicy,
|
OrganizationUser orgUserAnotherOrg, [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy,
|
||||||
string key, SutProvider<OrganizationService> sutProvider)
|
string key, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
|
||||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
|
||||||
|
org.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
orgUser.Status = OrganizationUserStatusType.Accepted;
|
||||||
|
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||||
|
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
||||||
|
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||||
|
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
||||||
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
|
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||||
|
singleOrgPolicy.OrganizationId = orgUserAnotherOrg.Id;
|
||||||
|
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(new[] { singleOrgPolicy });
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService));
|
||||||
|
Assert.Contains("Cannot confirm this member to the organization because they are in another organization which forbids it.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
|
public async Task ConfirmUser_AsOwnerOrAdmin_SingleOrgPolicy_ExcludedViaUserType_Success(
|
||||||
|
OrganizationUserType userType, Organization org, OrganizationUser confirmingUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||||
|
OrganizationUser orgUserAnotherOrg,
|
||||||
|
string key, SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
|
||||||
|
org.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
orgUser.Type = userType;
|
||||||
|
orgUser.Status = OrganizationUserStatusType.Accepted;
|
||||||
|
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||||
|
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
||||||
|
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||||
|
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
||||||
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
|
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||||
|
|
||||||
|
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
||||||
|
await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email);
|
||||||
|
await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ConfirmUser_TwoFactorPolicy_NotEnabled_Throws(Organization org, OrganizationUser confirmingUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||||
|
OrganizationUser orgUserAnotherOrg,
|
||||||
|
[OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,
|
||||||
|
string key, SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||||
var userService = Substitute.For<IUserService>();
|
var userService = Substitute.For<IUserService>();
|
||||||
|
|
||||||
org.PlanType = PlanType.EnterpriseAnnually;
|
org.PlanType = PlanType.EnterpriseAnnually;
|
||||||
@ -1486,7 +1520,8 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
||||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||||
policyRepository.GetManyByOrganizationIdAsync(org.Id).Returns(new[] { twoFactorPolicy });
|
twoFactorPolicy.OrganizationId = org.Id;
|
||||||
|
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService));
|
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService));
|
||||||
@ -1494,15 +1529,15 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task ConfirmUser_Success(Organization org, OrganizationUser confirmingUser,
|
public async Task ConfirmUser_TwoFactorPolicy_Enabled_Success(Organization org, OrganizationUser confirmingUser,
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||||
[Policy(PolicyType.TwoFactorAuthentication)] Policy twoFactorPolicy,
|
[OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,
|
||||||
[Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy, string key, SutProvider<OrganizationService> sutProvider)
|
string key, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
|
||||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||||
var userService = Substitute.For<IUserService>();
|
var userService = Substitute.For<IUserService>();
|
||||||
|
|
||||||
org.PlanType = PlanType.EnterpriseAnnually;
|
org.PlanType = PlanType.EnterpriseAnnually;
|
||||||
@ -1511,7 +1546,8 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||||
policyRepository.GetManyByOrganizationIdAsync(org.Id).Returns(new[] { twoFactorPolicy, singleOrgPolicy });
|
twoFactorPolicy.OrganizationId = org.Id;
|
||||||
|
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
|
||||||
userService.TwoFactorIsEnabledAsync(user).Returns(true);
|
userService.TwoFactorIsEnabledAsync(user).Returns(true);
|
||||||
|
|
||||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService);
|
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService);
|
||||||
@ -1524,13 +1560,14 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2,
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2,
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser3,
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser3,
|
||||||
OrganizationUser anotherOrgUser, User user1, User user2, User user3,
|
OrganizationUser anotherOrgUser, User user1, User user2, User user3,
|
||||||
[Policy(PolicyType.TwoFactorAuthentication)] Policy twoFactorPolicy,
|
[OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,
|
||||||
[Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy, string key, SutProvider<OrganizationService> sutProvider)
|
[OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy,
|
||||||
|
string key, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
|
||||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||||
var userService = Substitute.For<IUserService>();
|
var userService = Substitute.For<IUserService>();
|
||||||
|
|
||||||
org.PlanType = PlanType.EnterpriseAnnually;
|
org.PlanType = PlanType.EnterpriseAnnually;
|
||||||
@ -1543,10 +1580,14 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(orgUsers);
|
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(orgUsers);
|
||||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user1, user2, user3 });
|
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user1, user2, user3 });
|
||||||
policyRepository.GetManyByOrganizationIdAsync(org.Id).Returns(new[] { twoFactorPolicy, singleOrgPolicy });
|
twoFactorPolicy.OrganizationId = org.Id;
|
||||||
|
policyService.GetPoliciesApplicableToUserAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
|
||||||
userService.TwoFactorIsEnabledAsync(user1).Returns(true);
|
userService.TwoFactorIsEnabledAsync(user1).Returns(true);
|
||||||
userService.TwoFactorIsEnabledAsync(user2).Returns(false);
|
userService.TwoFactorIsEnabledAsync(user2).Returns(false);
|
||||||
userService.TwoFactorIsEnabledAsync(user3).Returns(true);
|
userService.TwoFactorIsEnabledAsync(user3).Returns(true);
|
||||||
|
singleOrgPolicy.OrganizationId = org.Id;
|
||||||
|
policyService.GetPoliciesApplicableToUserAsync(user3.Id, PolicyType.SingleOrg)
|
||||||
|
.Returns(new[] { singleOrgPolicy });
|
||||||
organizationUserRepository.GetManyByManyUsersAsync(default)
|
organizationUserRepository.GetManyByManyUsersAsync(default)
|
||||||
.ReturnsForAnyArgs(new[] { orgUser1, orgUser2, orgUser3, anotherOrgUser });
|
.ReturnsForAnyArgs(new[] { orgUser1, orgUser2, orgUser3, anotherOrgUser });
|
||||||
|
|
||||||
@ -1554,7 +1595,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
var result = await sutProvider.Sut.ConfirmUsersAsync(confirmingUser.OrganizationId, keys, confirmingUser.Id, userService);
|
var result = await sutProvider.Sut.ConfirmUsersAsync(confirmingUser.OrganizationId, keys, confirmingUser.Id, userService);
|
||||||
Assert.Contains("", result[0].Item2);
|
Assert.Contains("", result[0].Item2);
|
||||||
Assert.Contains("User does not have two-step login enabled.", result[1].Item2);
|
Assert.Contains("User does not have two-step login enabled.", result[1].Item2);
|
||||||
Assert.Contains("User is a member of another organization.", result[2].Item2);
|
Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Stripe;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Models.Business;
|
||||||
|
|
||||||
|
public class SeatSubscriptionUpdateTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2020)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2020)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2020)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2020)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually)]
|
||||||
|
[BitAutoData(PlanType.TeamsStarter)]
|
||||||
|
|
||||||
|
public void UpgradeItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization)
|
||||||
|
{
|
||||||
|
var plan = StaticStore.GetPlan(planType);
|
||||||
|
organization.PlanType = planType;
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "subscription_item",
|
||||||
|
Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId },
|
||||||
|
Quantity = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var update = new SeatSubscriptionUpdate(organization, plan, 100);
|
||||||
|
|
||||||
|
var options = update.UpgradeItemsOptions(subscription);
|
||||||
|
|
||||||
|
Assert.Single(options);
|
||||||
|
Assert.Equal(plan.PasswordManager.StripeSeatPlanId, options[0].Plan);
|
||||||
|
Assert.Equal(100, options[0].Quantity);
|
||||||
|
Assert.Null(options[0].Deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2020)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2020)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2020)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2020)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually)]
|
||||||
|
public void RevertItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization)
|
||||||
|
{
|
||||||
|
var plan = StaticStore.GetPlan(planType);
|
||||||
|
organization.PlanType = planType;
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "subscription_item",
|
||||||
|
Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId },
|
||||||
|
Quantity = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var update = new SeatSubscriptionUpdate(organization, plan, 100);
|
||||||
|
update.UpgradeItemsOptions(subscription);
|
||||||
|
|
||||||
|
var options = update.RevertItemsOptions(subscription);
|
||||||
|
|
||||||
|
Assert.Single(options);
|
||||||
|
Assert.Equal(plan.PasswordManager.StripeSeatPlanId, options[0].Plan);
|
||||||
|
Assert.Equal(organization.Seats, options[0].Quantity);
|
||||||
|
Assert.Null(options[0].Deleted);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Stripe;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Models.Business;
|
||||||
|
|
||||||
|
public class ServiceAccountSubscriptionUpdateTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2020)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2020)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2020)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2020)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually)]
|
||||||
|
[BitAutoData(PlanType.TeamsStarter)]
|
||||||
|
|
||||||
|
public void UpgradeItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization)
|
||||||
|
{
|
||||||
|
var plan = StaticStore.GetPlan(planType);
|
||||||
|
organization.PlanType = planType;
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "subscription_item",
|
||||||
|
Price = new Price { Id = plan.SecretsManager.StripeServiceAccountPlanId },
|
||||||
|
Quantity = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var update = new ServiceAccountSubscriptionUpdate(organization, plan, 3);
|
||||||
|
|
||||||
|
var options = update.UpgradeItemsOptions(subscription);
|
||||||
|
|
||||||
|
Assert.Single(options);
|
||||||
|
Assert.Equal(plan.SecretsManager.StripeServiceAccountPlanId, options[0].Plan);
|
||||||
|
Assert.Equal(3, options[0].Quantity);
|
||||||
|
Assert.Null(options[0].Deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2020)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2020)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2020)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2020)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually)]
|
||||||
|
public void RevertItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization)
|
||||||
|
{
|
||||||
|
var plan = StaticStore.GetPlan(planType);
|
||||||
|
organization.PlanType = planType;
|
||||||
|
var quantity = 5;
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "subscription_item",
|
||||||
|
Price = new Price { Id = plan.SecretsManager.StripeServiceAccountPlanId },
|
||||||
|
Quantity = quantity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var update = new ServiceAccountSubscriptionUpdate(organization, plan, quantity);
|
||||||
|
update.UpgradeItemsOptions(subscription);
|
||||||
|
|
||||||
|
var options = update.RevertItemsOptions(subscription);
|
||||||
|
|
||||||
|
Assert.Single(options);
|
||||||
|
Assert.Equal(plan.SecretsManager.StripeServiceAccountPlanId, options[0].Plan);
|
||||||
|
Assert.Equal(quantity, options[0].Quantity);
|
||||||
|
Assert.Null(options[0].Deleted);
|
||||||
|
}
|
||||||
|
}
|
101
test/Core.Test/Models/Business/SmSeatSubscriptionUpdateTests.cs
Normal file
101
test/Core.Test/Models/Business/SmSeatSubscriptionUpdateTests.cs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Stripe;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Models.Business;
|
||||||
|
|
||||||
|
public class SmSeatSubscriptionUpdateTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2020)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2020)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2020)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2020)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually)]
|
||||||
|
[BitAutoData(PlanType.TeamsStarter)]
|
||||||
|
|
||||||
|
public void UpgradeItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization)
|
||||||
|
{
|
||||||
|
var plan = StaticStore.GetPlan(planType);
|
||||||
|
organization.PlanType = planType;
|
||||||
|
var quantity = 3;
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "subscription_item",
|
||||||
|
Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId },
|
||||||
|
Quantity = quantity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var update = new SmSeatSubscriptionUpdate(organization, plan, quantity);
|
||||||
|
|
||||||
|
var options = update.UpgradeItemsOptions(subscription);
|
||||||
|
|
||||||
|
Assert.Single(options);
|
||||||
|
Assert.Equal(plan.SecretsManager.StripeSeatPlanId, options[0].Plan);
|
||||||
|
Assert.Equal(quantity, options[0].Quantity);
|
||||||
|
Assert.Null(options[0].Deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2020)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2020)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2020)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2020)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually)]
|
||||||
|
public void RevertItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization)
|
||||||
|
{
|
||||||
|
var plan = StaticStore.GetPlan(planType);
|
||||||
|
organization.PlanType = planType;
|
||||||
|
var quantity = 5;
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "subscription_item",
|
||||||
|
Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId },
|
||||||
|
Quantity = quantity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var update = new SmSeatSubscriptionUpdate(organization, plan, quantity);
|
||||||
|
update.UpgradeItemsOptions(subscription);
|
||||||
|
|
||||||
|
var options = update.RevertItemsOptions(subscription);
|
||||||
|
|
||||||
|
Assert.Single(options);
|
||||||
|
Assert.Equal(plan.SecretsManager.StripeSeatPlanId, options[0].Plan);
|
||||||
|
Assert.Equal(organization.SmSeats, options[0].Quantity);
|
||||||
|
Assert.Null(options[0].Deleted);
|
||||||
|
}
|
||||||
|
}
|
106
test/Core.Test/Models/Business/StorageSubscriptionUpdateTests.cs
Normal file
106
test/Core.Test/Models/Business/StorageSubscriptionUpdateTests.cs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Stripe;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Models.Business;
|
||||||
|
|
||||||
|
public class StorageSubscriptionUpdateTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2020)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2020)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2020)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2020)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually)]
|
||||||
|
[BitAutoData(PlanType.TeamsStarter)]
|
||||||
|
|
||||||
|
public void UpgradeItemsOptions_ReturnsCorrectOptions(PlanType planType)
|
||||||
|
{
|
||||||
|
var plan = StaticStore.GetPlan(planType);
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "subscription_item",
|
||||||
|
Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId },
|
||||||
|
Quantity = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var update = new StorageSubscriptionUpdate("plan_id", 100);
|
||||||
|
|
||||||
|
var options = update.UpgradeItemsOptions(subscription);
|
||||||
|
|
||||||
|
Assert.Single(options);
|
||||||
|
Assert.Equal("plan_id", options[0].Plan);
|
||||||
|
Assert.Equal(100, options[0].Quantity);
|
||||||
|
Assert.Null(options[0].Deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RevertItemsOptions_ThrowsExceptionIfPrevStorageIsNull()
|
||||||
|
{
|
||||||
|
var subscription = new Subscription();
|
||||||
|
var update = new StorageSubscriptionUpdate("plan_id", 100);
|
||||||
|
|
||||||
|
Assert.Throws<Exception>(() => update.RevertItemsOptions(subscription));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2020)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2020)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2020)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2020)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually)]
|
||||||
|
[BitAutoData(PlanType.TeamsStarter)]
|
||||||
|
public void RevertItemsOptions_ReturnsCorrectOptions(PlanType planType)
|
||||||
|
{
|
||||||
|
var plan = StaticStore.GetPlan(planType);
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "subscription_item",
|
||||||
|
Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId },
|
||||||
|
Quantity = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var update = new StorageSubscriptionUpdate(plan.PasswordManager.StripeStoragePlanId, 100);
|
||||||
|
update.UpgradeItemsOptions(subscription);
|
||||||
|
|
||||||
|
var options = update.RevertItemsOptions(subscription);
|
||||||
|
|
||||||
|
Assert.Single(options);
|
||||||
|
Assert.Equal(plan.PasswordManager.StripeStoragePlanId, options[0].Plan);
|
||||||
|
Assert.Equal(100, options[0].Quantity);
|
||||||
|
Assert.Null(options[0].Deleted);
|
||||||
|
}
|
||||||
|
}
|
@ -72,6 +72,113 @@ public class AuthRequestRepositoryTests
|
|||||||
Assert.Equal(4, numberOfDeleted);
|
Assert.Equal(4, numberOfDeleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task UpdateManyAsync_Works(
|
||||||
|
IAuthRequestRepository authRequestRepository,
|
||||||
|
IUserRepository userRepository)
|
||||||
|
{
|
||||||
|
// Create two distinct real users for foreign key requirements
|
||||||
|
var user1 = await userRepository.CreateAsync(new User
|
||||||
|
{
|
||||||
|
Name = "First Test User",
|
||||||
|
Email = $"test+{Guid.NewGuid()}@email.com",
|
||||||
|
ApiKey = "TEST",
|
||||||
|
SecurityStamp = "stamp",
|
||||||
|
});
|
||||||
|
|
||||||
|
var user2 = await userRepository.CreateAsync(new User
|
||||||
|
{
|
||||||
|
Name = "Second Test User",
|
||||||
|
Email = $"test+{Guid.NewGuid()}@email.com",
|
||||||
|
ApiKey = "TEST",
|
||||||
|
SecurityStamp = "stamp",
|
||||||
|
});
|
||||||
|
|
||||||
|
var user3 = await userRepository.CreateAsync(new User
|
||||||
|
{
|
||||||
|
Name = "Third Test User",
|
||||||
|
Email = $"test+{Guid.NewGuid()}@email.com",
|
||||||
|
ApiKey = "TEST",
|
||||||
|
SecurityStamp = "stamp",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create two different and still valid (not expired or responded to) auth requests
|
||||||
|
var authRequests = new List<AuthRequest>
|
||||||
|
{
|
||||||
|
await authRequestRepository.CreateAsync(CreateAuthRequest(user1.Id, AuthRequestType.AdminApproval, DateTime.UtcNow.AddMinutes(-5))),
|
||||||
|
await authRequestRepository.CreateAsync(CreateAuthRequest(user3.Id, AuthRequestType.AdminApproval, DateTime.UtcNow.AddMinutes(-7))),
|
||||||
|
await authRequestRepository.CreateAsync(CreateAuthRequest(user2.Id, AuthRequestType.AdminApproval, DateTime.UtcNow.AddMinutes(-10))),
|
||||||
|
// This last auth request is not created manually, and will be
|
||||||
|
// used to make sure entity framework's `UpdateRange` method
|
||||||
|
// doesn't create requests too.
|
||||||
|
CreateAuthRequest(user2.Id, AuthRequestType.AdminApproval, DateTime.UtcNow.AddMinutes(-11))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update some properties on two auth request, but leave the other one
|
||||||
|
// alone to be a control value
|
||||||
|
var authRequestToBeUpdated1 = authRequests[0];
|
||||||
|
var authRequestToBeUpdated2 = authRequests[1];
|
||||||
|
var authRequestNotToBeUpdated = authRequests[2];
|
||||||
|
authRequests[0].Approved = true;
|
||||||
|
authRequests[0].ResponseDate = DateTime.UtcNow.AddMinutes(-1);
|
||||||
|
authRequests[0].Key = "UPDATED_KEY_1";
|
||||||
|
authRequests[0].MasterPasswordHash = "UPDATED_MASTERPASSWORDHASH_1";
|
||||||
|
|
||||||
|
authRequests[1].Approved = false;
|
||||||
|
authRequests[1].ResponseDate = DateTime.UtcNow.AddMinutes(-2);
|
||||||
|
|
||||||
|
// Run the method being tested
|
||||||
|
await authRequestRepository.UpdateManyAsync(authRequests);
|
||||||
|
|
||||||
|
// Define what "Equality" really means in this context
|
||||||
|
// This includes stripping milliseconds off of dates, because we can't
|
||||||
|
// reliably compare that deep
|
||||||
|
static DateTime? TrimMilliseconds(DateTime? dt)
|
||||||
|
{
|
||||||
|
if (!dt.HasValue)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new DateTime(dt.Value.Year, dt.Value.Month, dt.Value.Day, dt.Value.Hour, dt.Value.Minute, dt.Value.Second, 0, dt.Value.Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AuthRequestEquals(AuthRequest x, AuthRequest y)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
x.Id == y.Id &&
|
||||||
|
x.UserId == y.UserId &&
|
||||||
|
x.Type == y.Type &&
|
||||||
|
x.RequestDeviceIdentifier == y.RequestDeviceIdentifier &&
|
||||||
|
x.RequestDeviceType == y.RequestDeviceType &&
|
||||||
|
x.RequestIpAddress == y.RequestIpAddress &&
|
||||||
|
x.ResponseDeviceId == y.ResponseDeviceId &&
|
||||||
|
x.AccessCode == y.AccessCode &&
|
||||||
|
x.PublicKey == y.PublicKey &&
|
||||||
|
x.Key == y.Key &&
|
||||||
|
x.MasterPasswordHash == y.MasterPasswordHash &&
|
||||||
|
x.Approved == y.Approved &&
|
||||||
|
TrimMilliseconds(x.CreationDate) == TrimMilliseconds(y.CreationDate) &&
|
||||||
|
TrimMilliseconds(x.ResponseDate) == TrimMilliseconds(y.ResponseDate) &&
|
||||||
|
TrimMilliseconds(x.AuthenticationDate) == TrimMilliseconds(y.AuthenticationDate) &&
|
||||||
|
x.OrganizationId == y.OrganizationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that the unchanged auth request is still unchanged
|
||||||
|
var skippedAuthRequest = await authRequestRepository.GetByIdAsync(authRequestNotToBeUpdated.Id);
|
||||||
|
Assert.True(AuthRequestEquals(skippedAuthRequest, authRequestNotToBeUpdated));
|
||||||
|
|
||||||
|
// Assert that the values updated on the changed auth requests were updated, and no others
|
||||||
|
var updatedAuthRequest1 = await authRequestRepository.GetByIdAsync(authRequestToBeUpdated1.Id);
|
||||||
|
Assert.True(AuthRequestEquals(authRequestToBeUpdated1, updatedAuthRequest1));
|
||||||
|
var updatedAuthRequest2 = await authRequestRepository.GetByIdAsync(authRequestToBeUpdated2.Id);
|
||||||
|
Assert.True(AuthRequestEquals(authRequestToBeUpdated2, updatedAuthRequest2));
|
||||||
|
|
||||||
|
// Assert that the auth request we never created is not created by
|
||||||
|
// the update method.
|
||||||
|
var uncreatedAuthRequest = await authRequestRepository.GetByIdAsync(authRequests[3].Id);
|
||||||
|
Assert.Null(uncreatedAuthRequest);
|
||||||
|
}
|
||||||
|
|
||||||
private static AuthRequest CreateAuthRequest(Guid userId, AuthRequestType authRequestType, DateTime creationDate, bool? approved = null, DateTime? responseDate = null)
|
private static AuthRequest CreateAuthRequest(Guid userId, AuthRequestType authRequestType, DateTime creationDate, bool? approved = null, DateTime? responseDate = null)
|
||||||
{
|
{
|
||||||
return new AuthRequest
|
return new AuthRequest
|
||||||
|
@ -310,6 +310,7 @@ public class CollectionRepositoryTests
|
|||||||
Assert.True(c1.Manage);
|
Assert.True(c1.Manage);
|
||||||
Assert.False(c1.ReadOnly);
|
Assert.False(c1.ReadOnly);
|
||||||
Assert.False(c1.HidePasswords);
|
Assert.False(c1.HidePasswords);
|
||||||
|
Assert.False(c1.Unmanaged);
|
||||||
}, c2 =>
|
}, c2 =>
|
||||||
{
|
{
|
||||||
Assert.NotNull(c2);
|
Assert.NotNull(c2);
|
||||||
@ -319,6 +320,7 @@ public class CollectionRepositoryTests
|
|||||||
Assert.False(c2.Manage);
|
Assert.False(c2.Manage);
|
||||||
Assert.True(c2.ReadOnly);
|
Assert.True(c2.ReadOnly);
|
||||||
Assert.False(c2.HidePasswords);
|
Assert.False(c2.HidePasswords);
|
||||||
|
Assert.True(c2.Unmanaged);
|
||||||
}, c3 =>
|
}, c3 =>
|
||||||
{
|
{
|
||||||
Assert.NotNull(c3);
|
Assert.NotNull(c3);
|
||||||
@ -328,6 +330,7 @@ public class CollectionRepositoryTests
|
|||||||
Assert.False(c3.Manage);
|
Assert.False(c3.Manage);
|
||||||
Assert.False(c3.ReadOnly);
|
Assert.False(c3.ReadOnly);
|
||||||
Assert.False(c3.HidePasswords);
|
Assert.False(c3.HidePasswords);
|
||||||
|
Assert.False(c3.Unmanaged);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -436,6 +439,7 @@ public class CollectionRepositoryTests
|
|||||||
Assert.True(c1.Manage);
|
Assert.True(c1.Manage);
|
||||||
Assert.False(c1.ReadOnly);
|
Assert.False(c1.ReadOnly);
|
||||||
Assert.False(c1.HidePasswords);
|
Assert.False(c1.HidePasswords);
|
||||||
|
Assert.False(c1.Unmanaged);
|
||||||
}, c2 =>
|
}, c2 =>
|
||||||
{
|
{
|
||||||
Assert.NotNull(c2);
|
Assert.NotNull(c2);
|
||||||
@ -445,6 +449,7 @@ public class CollectionRepositoryTests
|
|||||||
Assert.False(c2.Manage);
|
Assert.False(c2.Manage);
|
||||||
Assert.True(c2.ReadOnly);
|
Assert.True(c2.ReadOnly);
|
||||||
Assert.False(c2.HidePasswords);
|
Assert.False(c2.HidePasswords);
|
||||||
|
Assert.True(c2.Unmanaged);
|
||||||
}, c3 =>
|
}, c3 =>
|
||||||
{
|
{
|
||||||
Assert.NotNull(c3);
|
Assert.NotNull(c3);
|
||||||
@ -454,6 +459,7 @@ public class CollectionRepositoryTests
|
|||||||
Assert.True(c3.Manage); // Group 2 is Manage
|
Assert.True(c3.Manage); // Group 2 is Manage
|
||||||
Assert.False(c3.ReadOnly);
|
Assert.False(c3.ReadOnly);
|
||||||
Assert.False(c3.HidePasswords);
|
Assert.False(c3.HidePasswords);
|
||||||
|
Assert.False(c3.Unmanaged);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
CREATE PROCEDURE AuthRequest_UpdateMany
|
||||||
|
@jsonData NVARCHAR(MAX)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
UPDATE AR
|
||||||
|
SET
|
||||||
|
[Id] = ARI.[Id],
|
||||||
|
[UserId] = ARI.[UserId],
|
||||||
|
[Type] = ARI.[Type],
|
||||||
|
[RequestDeviceIdentifier] = ARI.[RequestDeviceIdentifier],
|
||||||
|
[RequestDeviceType] = ARI.[RequestDeviceType],
|
||||||
|
[RequestIpAddress] = ARI.[RequestIpAddress],
|
||||||
|
[ResponseDeviceId] = ARI.[ResponseDeviceId],
|
||||||
|
[AccessCode] = ARI.[AccessCode],
|
||||||
|
[PublicKey] = ARI.[PublicKey],
|
||||||
|
[Key] = ARI.[Key],
|
||||||
|
[MasterPasswordHash] = ARI.[MasterPasswordHash],
|
||||||
|
[Approved] = ARI.[Approved],
|
||||||
|
[CreationDate] = ARI.[CreationDate],
|
||||||
|
[ResponseDate] = ARI.[ResponseDate],
|
||||||
|
[AuthenticationDate] = ARI.[AuthenticationDate],
|
||||||
|
[OrganizationId] = ARI.[OrganizationId]
|
||||||
|
FROM
|
||||||
|
[dbo].[AuthRequest] AR
|
||||||
|
INNER JOIN
|
||||||
|
OPENJSON(@jsonData)
|
||||||
|
WITH (
|
||||||
|
Id UNIQUEIDENTIFIER '$.Id',
|
||||||
|
UserId UNIQUEIDENTIFIER '$.UserId',
|
||||||
|
Type SMALLINT '$.Type',
|
||||||
|
RequestDeviceIdentifier NVARCHAR(50) '$.RequestDeviceIdentifier',
|
||||||
|
RequestDeviceType SMALLINT '$.RequestDeviceType',
|
||||||
|
RequestIpAddress VARCHAR(50) '$.RequestIpAddress',
|
||||||
|
ResponseDeviceId UNIQUEIDENTIFIER '$.ResponseDeviceId',
|
||||||
|
AccessCode VARCHAR(25) '$.AccessCode',
|
||||||
|
PublicKey VARCHAR(MAX) '$.PublicKey',
|
||||||
|
[Key] VARCHAR(MAX) '$.Key',
|
||||||
|
MasterPasswordHash VARCHAR(MAX) '$.MasterPasswordHash',
|
||||||
|
Approved BIT '$.Approved',
|
||||||
|
CreationDate DATETIME2 '$.CreationDate',
|
||||||
|
ResponseDate DATETIME2 '$.ResponseDate',
|
||||||
|
AuthenticationDate DATETIME2 '$.AuthenticationDate',
|
||||||
|
OrganizationId UNIQUEIDENTIFIER '$.OrganizationId'
|
||||||
|
) ARI ON AR.Id = ARI.Id;
|
||||||
|
END
|
@ -0,0 +1,171 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadByOrganizationIdWithPermissions]
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@UserId UNIQUEIDENTIFIER,
|
||||||
|
@IncludeAccessRelationships BIT
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
C.*,
|
||||||
|
MIN(CASE
|
||||||
|
WHEN
|
||||||
|
COALESCE(CU.[ReadOnly], CG.[ReadOnly], 0) = 0
|
||||||
|
THEN 0
|
||||||
|
ELSE 1
|
||||||
|
END) AS [ReadOnly],
|
||||||
|
MIN(CASE
|
||||||
|
WHEN
|
||||||
|
COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) = 0
|
||||||
|
THEN 0
|
||||||
|
ELSE 1
|
||||||
|
END) AS [HidePasswords],
|
||||||
|
MAX(CASE
|
||||||
|
WHEN
|
||||||
|
COALESCE(CU.[Manage], CG.[Manage], 0) = 0
|
||||||
|
THEN 0
|
||||||
|
ELSE 1
|
||||||
|
END) AS [Manage],
|
||||||
|
MAX(CASE
|
||||||
|
WHEN
|
||||||
|
CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL
|
||||||
|
THEN 0
|
||||||
|
ELSE 1
|
||||||
|
END) AS [Assigned],
|
||||||
|
CASE
|
||||||
|
WHEN
|
||||||
|
-- No active user or group has manage rights
|
||||||
|
NOT EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM [dbo].[CollectionUser] CU2
|
||||||
|
JOIN [dbo].[OrganizationUser] OU2 ON CU2.[OrganizationUserId] = OU2.[Id]
|
||||||
|
WHERE
|
||||||
|
CU2.[CollectionId] = C.[Id] AND
|
||||||
|
OU2.[Status] = 2 AND
|
||||||
|
CU2.[Manage] = 1
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM [dbo].[CollectionGroup] CG2
|
||||||
|
WHERE
|
||||||
|
CG2.[CollectionId] = C.[Id] AND
|
||||||
|
CG2.[Manage] = 1
|
||||||
|
)
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS [Unmanaged]
|
||||||
|
FROM
|
||||||
|
[dbo].[CollectionView] C
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[OrganizationUser] OU ON C.[OrganizationId] = OU.[OrganizationId] AND OU.[UserId] = @UserId
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[CollectionUser] CU ON CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = [OU].[Id]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[Group] G ON G.[Id] = GU.[GroupId]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId]
|
||||||
|
WHERE
|
||||||
|
C.[OrganizationId] = @OrganizationId
|
||||||
|
GROUP BY
|
||||||
|
C.[Id],
|
||||||
|
C.[OrganizationId],
|
||||||
|
C.[Name],
|
||||||
|
C.[CreationDate],
|
||||||
|
C.[RevisionDate],
|
||||||
|
C.[ExternalId]
|
||||||
|
|
||||||
|
IF (@IncludeAccessRelationships = 1)
|
||||||
|
BEGIN
|
||||||
|
EXEC [dbo].[CollectionGroup_ReadByOrganizationId] @OrganizationId
|
||||||
|
EXEC [dbo].[CollectionUser_ReadByOrganizationId] @OrganizationId
|
||||||
|
END
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadByIdWithPermissions]
|
||||||
|
@CollectionId UNIQUEIDENTIFIER,
|
||||||
|
@UserId UNIQUEIDENTIFIER,
|
||||||
|
@IncludeAccessRelationships BIT
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
C.*,
|
||||||
|
MIN(CASE
|
||||||
|
WHEN
|
||||||
|
COALESCE(CU.[ReadOnly], CG.[ReadOnly], 0) = 0
|
||||||
|
THEN 0
|
||||||
|
ELSE 1
|
||||||
|
END) AS [ReadOnly],
|
||||||
|
MIN (CASE
|
||||||
|
WHEN
|
||||||
|
COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) = 0
|
||||||
|
THEN 0
|
||||||
|
ELSE 1
|
||||||
|
END) AS [HidePasswords],
|
||||||
|
MAX(CASE
|
||||||
|
WHEN
|
||||||
|
COALESCE(CU.[Manage], CG.[Manage], 0) = 0
|
||||||
|
THEN 0
|
||||||
|
ELSE 1
|
||||||
|
END) AS [Manage],
|
||||||
|
MAX(CASE
|
||||||
|
WHEN
|
||||||
|
CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL
|
||||||
|
THEN 0
|
||||||
|
ELSE 1
|
||||||
|
END) AS [Assigned],
|
||||||
|
CASE
|
||||||
|
WHEN
|
||||||
|
-- No active user or group has manage rights
|
||||||
|
NOT EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM [dbo].[CollectionUser] CU2
|
||||||
|
JOIN [dbo].[OrganizationUser] OU2 ON CU2.[OrganizationUserId] = OU2.[Id]
|
||||||
|
WHERE
|
||||||
|
CU2.[CollectionId] = C.[Id] AND
|
||||||
|
OU2.[Status] = 2 AND
|
||||||
|
CU2.[Manage] = 1
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM [dbo].[CollectionGroup] CG2
|
||||||
|
WHERE
|
||||||
|
CG2.[CollectionId] = C.[Id] AND
|
||||||
|
CG2.[Manage] = 1
|
||||||
|
)
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS [Unmanaged]
|
||||||
|
FROM
|
||||||
|
[dbo].[CollectionView] C
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[OrganizationUser] OU ON C.[OrganizationId] = OU.[OrganizationId] AND OU.[UserId] = @UserId
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[CollectionUser] CU ON CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = [OU].[Id]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[Group] G ON G.[Id] = GU.[GroupId]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId]
|
||||||
|
WHERE
|
||||||
|
C.[Id] = @CollectionId
|
||||||
|
GROUP BY
|
||||||
|
C.[Id],
|
||||||
|
C.[OrganizationId],
|
||||||
|
C.[Name],
|
||||||
|
C.[CreationDate],
|
||||||
|
C.[RevisionDate],
|
||||||
|
C.[ExternalId]
|
||||||
|
|
||||||
|
IF (@IncludeAccessRelationships = 1)
|
||||||
|
BEGIN
|
||||||
|
EXEC [dbo].[CollectionGroup_ReadByCollectionId] @CollectionId
|
||||||
|
EXEC [dbo].[CollectionUser_ReadByCollectionId] @CollectionId
|
||||||
|
END
|
||||||
|
END
|
||||||
|
GO
|
124
util/Migrator/DbScripts/2024-05-20_00_FixManageAggregation.sql
Normal file
124
util/Migrator/DbScripts/2024-05-20_00_FixManageAggregation.sql
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
-- We were aggregating CollectionGroup permissions using MIN([Manage]) instead of MAX.
|
||||||
|
-- If the user is a member of multiple groups with overlapping collection permissions, they should get the most
|
||||||
|
-- generous permissions, not the least. This is consistent with ReadOnly and HidePasswords columns.
|
||||||
|
-- Updating both current and V2 sprocs out of caution and because they still need to be reviewed/cleaned up.
|
||||||
|
|
||||||
|
-- Collection_ReadByIdUserId
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadByIdUserId]
|
||||||
|
@Id UNIQUEIDENTIFIER,
|
||||||
|
@UserId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
OrganizationId,
|
||||||
|
[Name],
|
||||||
|
CreationDate,
|
||||||
|
RevisionDate,
|
||||||
|
ExternalId,
|
||||||
|
MIN([ReadOnly]) AS [ReadOnly],
|
||||||
|
MIN([HidePasswords]) AS [HidePasswords],
|
||||||
|
MAX([Manage]) AS [Manage]
|
||||||
|
FROM
|
||||||
|
[dbo].[UserCollectionDetails](@UserId)
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
GROUP BY
|
||||||
|
Id,
|
||||||
|
OrganizationId,
|
||||||
|
[Name],
|
||||||
|
CreationDate,
|
||||||
|
RevisionDate,
|
||||||
|
ExternalId
|
||||||
|
END
|
||||||
|
GO;
|
||||||
|
|
||||||
|
-- Collection_ReadByIdUserId_V2
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadByIdUserId_V2]
|
||||||
|
@Id UNIQUEIDENTIFIER,
|
||||||
|
@UserId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
OrganizationId,
|
||||||
|
[Name],
|
||||||
|
CreationDate,
|
||||||
|
RevisionDate,
|
||||||
|
ExternalId,
|
||||||
|
MIN([ReadOnly]) AS [ReadOnly],
|
||||||
|
MIN([HidePasswords]) AS [HidePasswords],
|
||||||
|
MAX([Manage]) AS [Manage]
|
||||||
|
FROM
|
||||||
|
[dbo].[UserCollectionDetails_V2](@UserId)
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
GROUP BY
|
||||||
|
Id,
|
||||||
|
OrganizationId,
|
||||||
|
[Name],
|
||||||
|
CreationDate,
|
||||||
|
RevisionDate,
|
||||||
|
ExternalId
|
||||||
|
END
|
||||||
|
GO;
|
||||||
|
|
||||||
|
-- Collection_ReadByUserId
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadByUserId]
|
||||||
|
@UserId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
OrganizationId,
|
||||||
|
[Name],
|
||||||
|
CreationDate,
|
||||||
|
RevisionDate,
|
||||||
|
ExternalId,
|
||||||
|
MIN([ReadOnly]) AS [ReadOnly],
|
||||||
|
MIN([HidePasswords]) AS [HidePasswords],
|
||||||
|
MAX([Manage]) AS [Manage]
|
||||||
|
FROM
|
||||||
|
[dbo].[UserCollectionDetails](@UserId)
|
||||||
|
GROUP BY
|
||||||
|
Id,
|
||||||
|
OrganizationId,
|
||||||
|
[Name],
|
||||||
|
CreationDate,
|
||||||
|
RevisionDate,
|
||||||
|
ExternalId
|
||||||
|
END
|
||||||
|
GO;
|
||||||
|
|
||||||
|
-- Collection_ReadByUserId_V2
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadByUserId_V2]
|
||||||
|
@UserId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
OrganizationId,
|
||||||
|
[Name],
|
||||||
|
CreationDate,
|
||||||
|
RevisionDate,
|
||||||
|
ExternalId,
|
||||||
|
MIN([ReadOnly]) AS [ReadOnly],
|
||||||
|
MIN([HidePasswords]) AS [HidePasswords],
|
||||||
|
MAX([Manage]) AS [Manage]
|
||||||
|
FROM
|
||||||
|
[dbo].[UserCollectionDetails_V2](@UserId)
|
||||||
|
GROUP BY
|
||||||
|
Id,
|
||||||
|
OrganizationId,
|
||||||
|
[Name],
|
||||||
|
CreationDate,
|
||||||
|
RevisionDate,
|
||||||
|
ExternalId
|
||||||
|
END
|
||||||
|
GO;
|
@ -0,0 +1,42 @@
|
|||||||
|
-- This script will enable collection enhancements for organizations that don't have Collection Enhancements enabled.
|
||||||
|
-- This is a copy/paste of an earlier migration script: 2024-04-25_00_EnableAllOrgCollectionEnhancements.sql.
|
||||||
|
-- The earlier migration was accidentally released for self-host before the feature was enabled for new organizations,
|
||||||
|
-- so there was a window in time where existing self-host organizations were migrated, but it was still possible to create
|
||||||
|
-- a new organization that needed migration.
|
||||||
|
-- This script is being re-run to catch any organizations created during that window.
|
||||||
|
|
||||||
|
-- Step 1: Create a temporary table to store the Organizations with FlexibleCollections = 0
|
||||||
|
SELECT [Id] AS [OrganizationId]
|
||||||
|
INTO #TempOrg
|
||||||
|
FROM [dbo].[Organization]
|
||||||
|
WHERE [FlexibleCollections] = 0
|
||||||
|
|
||||||
|
-- Step 2: Execute the stored procedure for each OrganizationId
|
||||||
|
DECLARE @OrganizationId UNIQUEIDENTIFIER;
|
||||||
|
|
||||||
|
DECLARE OrgCursor CURSOR FOR
|
||||||
|
SELECT [OrganizationId]
|
||||||
|
FROM #TempOrg;
|
||||||
|
|
||||||
|
OPEN OrgCursor;
|
||||||
|
|
||||||
|
FETCH NEXT FROM OrgCursor INTO @OrganizationId;
|
||||||
|
|
||||||
|
WHILE (@@FETCH_STATUS = 0)
|
||||||
|
BEGIN
|
||||||
|
-- Execute the stored procedure for the current OrganizationId
|
||||||
|
EXEC [dbo].[Organization_EnableCollectionEnhancements] @OrganizationId;
|
||||||
|
|
||||||
|
-- Update the Organization to set FlexibleCollections = 1
|
||||||
|
UPDATE [dbo].[Organization]
|
||||||
|
SET [FlexibleCollections] = 1
|
||||||
|
WHERE [Id] = @OrganizationId;
|
||||||
|
|
||||||
|
FETCH NEXT FROM OrgCursor INTO @OrganizationId;
|
||||||
|
END;
|
||||||
|
|
||||||
|
CLOSE OrgCursor;
|
||||||
|
DEALLOCATE OrgCursor;
|
||||||
|
|
||||||
|
-- Step 3: Drop the temporary table
|
||||||
|
DROP TABLE #TempOrg;
|
Loading…
x
Reference in New Issue
Block a user