1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-03 09:02:48 -05:00

Merge branch 'main' into ac/ac-1682/data-migrations-for-deprecated-permissions

This commit is contained in:
Rui Tome
2024-01-17 13:31:21 +00:00
80 changed files with 1777 additions and 656 deletions

View File

@ -7,7 +7,7 @@
"commands": ["swagger"] "commands": ["swagger"]
}, },
"dotnet-ef": { "dotnet-ef": {
"version": "7.0.14", "version": "7.0.15",
"commands": ["dotnet-ef"] "commands": ["dotnet-ef"]
} }
} }

View File

@ -203,10 +203,5 @@
"reviewers": ["team:team-vault-dev"] "reviewers": ["team:team-vault-dev"]
} }
], ],
"force": {
"constraints": {
"dotnet": "6.0.100"
}
},
"ignoreDeps": ["dotnet-sdk"] "ignoreDeps": ["dotnet-sdk"]
} }

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Version>2024.1.0</Version> <Version>2024.1.2</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace> <RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>

View File

@ -0,0 +1,98 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.Extensions.Logging;
using Stripe;
namespace Bit.Commercial.Core.AdminConsole.Providers;
public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProviderCommand
{
private readonly IEventService _eventService;
private readonly ILogger<RemoveOrganizationFromProviderCommand> _logger;
private readonly IMailService _mailService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationService _organizationService;
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IStripeAdapter _stripeAdapter;
public RemoveOrganizationFromProviderCommand(
IEventService eventService,
ILogger<RemoveOrganizationFromProviderCommand> logger,
IMailService mailService,
IOrganizationRepository organizationRepository,
IOrganizationService organizationService,
IProviderOrganizationRepository providerOrganizationRepository,
IStripeAdapter stripeAdapter)
{
_eventService = eventService;
_logger = logger;
_mailService = mailService;
_organizationRepository = organizationRepository;
_organizationService = organizationService;
_providerOrganizationRepository = providerOrganizationRepository;
_stripeAdapter = stripeAdapter;
}
public async Task RemoveOrganizationFromProvider(
Provider provider,
ProviderOrganization providerOrganization,
Organization organization)
{
if (provider == null ||
providerOrganization == null ||
organization == null ||
providerOrganization.ProviderId != provider.Id)
{
throw new BadRequestException("Failed to remove organization. Please contact support.");
}
if (!await _organizationService.HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
Array.Empty<Guid>(),
includeProvider: false))
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
}
var organizationOwnerEmails =
(await _organizationRepository.GetOwnerEmailAddressesById(organization.Id)).ToList();
organization.BillingEmail = organizationOwnerEmails.MinBy(email => email);
await _organizationRepository.ReplaceAsync(organization);
var customerUpdateOptions = new CustomerUpdateOptions
{
Coupon = string.Empty,
Email = organization.BillingEmail
};
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, customerUpdateOptions);
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
{
CollectionMethod = "send_invoice",
DaysUntilDue = 30
};
await _stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, subscriptionUpdateOptions);
await _mailService.SendProviderUpdatePaymentMethod(
organization.Id,
organization.Name,
provider.Name,
organizationOwnerEmails);
await _providerOrganizationRepository.DeleteAsync(providerOrganization);
await _eventService.LogProviderOrganizationEventAsync(
providerOrganization,
EventType.ProviderOrganization_Removed);
}
}

View File

@ -527,23 +527,6 @@ public class ProviderService : IProviderService
return providerOrganization; return providerOrganization;
} }
public async Task RemoveOrganizationAsync(Guid providerId, Guid providerOrganizationId, Guid removingUserId)
{
var providerOrganization = await _providerOrganizationRepository.GetByIdAsync(providerOrganizationId);
if (providerOrganization == null || providerOrganization.ProviderId != providerId)
{
throw new BadRequestException("Invalid organization.");
}
if (!await _organizationService.HasConfirmedOwnersExceptAsync(providerOrganization.OrganizationId, new Guid[] { }, includeProvider: false))
{
throw new BadRequestException("Organization needs to have at least one confirmed owner.");
}
await _providerOrganizationRepository.DeleteAsync(providerOrganization);
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);
}
public async Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid ownerId) public async Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid ownerId)
{ {
var provider = await _providerRepository.GetByIdAsync(providerId); var provider = await _providerRepository.GetByIdAsync(providerId);

View File

@ -12,5 +12,6 @@ public static class ServiceCollectionExtensions
{ {
services.AddScoped<IProviderService, ProviderService>(); services.AddScoped<IProviderService, ProviderService>();
services.AddScoped<ICreateProviderCommand, CreateProviderCommand>(); services.AddScoped<ICreateProviderCommand, CreateProviderCommand>();
services.AddScoped<IRemoveOrganizationFromProviderCommand, RemoveOrganizationFromProviderCommand>();
} }
} }

View File

@ -0,0 +1,132 @@
using Bit.Commercial.Core.AdminConsole.Providers;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Stripe;
using Xunit;
namespace Bit.Commercial.Core.Test.AdminConsole.ProviderFeatures;
[SutProviderCustomize]
public class RemoveOrganizationFromProviderCommandTests
{
[Theory, BitAutoData]
public async Task RemoveOrganizationFromProvider_NoProvider_BadRequest(
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
{
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(null, null, null));
Assert.Equal("Failed to remove organization. Please contact support.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveOrganizationFromProvider_NoProviderOrganization_BadRequest(
Provider provider,
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
{
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(provider, null, null));
Assert.Equal("Failed to remove organization. Please contact support.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveOrganizationFromProvider_NoOrganization_BadRequest(
Provider provider,
ProviderOrganization providerOrganization,
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
{
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(
provider, providerOrganization, null));
Assert.Equal("Failed to remove organization. Please contact support.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveOrganizationFromProvider_MismatchedProviderOrganization_BadRequest(
Provider provider,
ProviderOrganization providerOrganization,
Organization organization,
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
{
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization));
Assert.Equal("Failed to remove organization. Please contact support.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveOrganizationFromProvider_NoConfirmedOwners_BadRequest(
Provider provider,
ProviderOrganization providerOrganization,
Organization organization,
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
{
providerOrganization.ProviderId = provider.Id;
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
Array.Empty<Guid>(),
includeProvider: false)
.Returns(false);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization));
Assert.Equal("Organization must have at least one confirmed owner.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveOrganizationFromProvider_MakesCorrectInvocations(
Provider provider,
ProviderOrganization providerOrganization,
Organization organization,
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
{
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.Received(1).CustomerUpdateAsync(
organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
options => options.Coupon == string.Empty && options.Email == "a@gmail.com"));
await stripeAdapter.Received(1).SubscriptionUpdateAsync(
organization.GatewaySubscriptionId, Arg.Is<SubscriptionUpdateOptions>(
options => options.CollectionMethod == "send_invoice" && options.DaysUntilDue == 30));
await sutProvider.GetDependency<IMailService>().Received(1).SendProviderUpdatePaymentMethod(
organization.Id,
organization.Name,
provider.Name,
Arg.Is<IEnumerable<string>>(emails => emails.Contains("a@gmail.com") && emails.Contains("b@gmail.com")));
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
.DeleteAsync(providerOrganization);
await sutProvider.GetDependency<IEventService>().Received(1).LogProviderOrganizationEventAsync(
providerOrganization,
EventType.ProviderOrganization_Removed);
}
}

View File

@ -541,65 +541,6 @@ public class ProviderServiceTests
t.First().Item2 == null)); t.First().Item2 == null));
} }
[Theory, BitAutoData]
public async Task RemoveOrganization_ProviderOrganizationIsInvalid_Throws(Provider provider,
ProviderOrganization providerOrganization, User user, SutProvider<ProviderService> sutProvider)
{
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganization.Id)
.ReturnsNull();
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveOrganizationAsync(provider.Id, providerOrganization.Id, user.Id));
Assert.Equal("Invalid organization.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveOrganization_ProviderOrganizationBelongsToWrongProvider_Throws(Provider provider,
ProviderOrganization providerOrganization, User user, SutProvider<ProviderService> sutProvider)
{
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganization.Id)
.Returns(providerOrganization);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveOrganizationAsync(provider.Id, providerOrganization.Id, user.Id));
Assert.Equal("Invalid organization.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveOrganization_HasNoOwners_Throws(Provider provider,
ProviderOrganization providerOrganization, User user, SutProvider<ProviderService> sutProvider)
{
providerOrganization.ProviderId = provider.Id;
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganization.Id)
.Returns(providerOrganization);
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(default, default, default)
.ReturnsForAnyArgs(false);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveOrganizationAsync(provider.Id, providerOrganization.Id, user.Id));
Assert.Equal("Organization needs to have at least one confirmed owner.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveOrganization_Success(Provider provider,
ProviderOrganization providerOrganization, User user, SutProvider<ProviderService> sutProvider)
{
providerOrganization.ProviderId = provider.Id;
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
providerOrganizationRepository.GetByIdAsync(providerOrganization.Id).Returns(providerOrganization);
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(default, default, default)
.ReturnsForAnyArgs(true);
await sutProvider.Sut.RemoveOrganizationAsync(provider.Id, providerOrganization.Id, user.Id);
await providerOrganizationRepository.Received().DeleteAsync(providerOrganization);
await sutProvider.GetDependency<IEventService>().Received()
.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);
}
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task AddOrganization_CreateAfterNov162023_PlanTypeDoesNotUpdated(Provider provider, Organization organization, string key, public async Task AddOrganization_CreateAfterNov162023_PlanTypeDoesNotUpdated(Provider provider, Organization organization, string key,
SutProvider<ProviderService> sutProvider) SutProvider<ProviderService> sutProvider)

View File

@ -8,7 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.11" /> <PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -3,7 +3,9 @@ using Bit.Admin.Models;
using Bit.Admin.Services; using Bit.Admin.Services;
using Bit.Admin.Utilities; using Bit.Admin.Utilities;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Commands;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -48,6 +50,9 @@ public class OrganizationsController : Controller
private readonly ISecretRepository _secretRepository; private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository; private readonly IProjectRepository _projectRepository;
private readonly IServiceAccountRepository _serviceAccountRepository; private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
private readonly IRemovePaymentMethodCommand _removePaymentMethodCommand;
public OrganizationsController( public OrganizationsController(
IOrganizationService organizationService, IOrganizationService organizationService,
@ -71,7 +76,10 @@ public class OrganizationsController : Controller
ICurrentContext currentContext, ICurrentContext currentContext,
ISecretRepository secretRepository, ISecretRepository secretRepository,
IProjectRepository projectRepository, IProjectRepository projectRepository,
IServiceAccountRepository serviceAccountRepository) IServiceAccountRepository serviceAccountRepository,
IProviderOrganizationRepository providerOrganizationRepository,
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
IRemovePaymentMethodCommand removePaymentMethodCommand)
{ {
_organizationService = organizationService; _organizationService = organizationService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
@ -95,6 +103,9 @@ public class OrganizationsController : Controller
_secretRepository = secretRepository; _secretRepository = secretRepository;
_projectRepository = projectRepository; _projectRepository = projectRepository;
_serviceAccountRepository = serviceAccountRepository; _serviceAccountRepository = serviceAccountRepository;
_providerOrganizationRepository = providerOrganizationRepository;
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
_removePaymentMethodCommand = removePaymentMethodCommand;
} }
[RequirePermission(Permission.Org_List_View)] [RequirePermission(Permission.Org_List_View)]
@ -286,6 +297,38 @@ public class OrganizationsController : Controller
return Json(null); return Json(null);
} }
[HttpPost]
[RequirePermission(Permission.Provider_Edit)]
public async Task<IActionResult> UnlinkOrganizationFromProviderAsync(Guid id)
{
var organization = await _organizationRepository.GetByIdAsync(id);
if (organization is null)
{
return RedirectToAction("Index");
}
var provider = await _providerRepository.GetByOrganizationIdAsync(id);
if (provider is null)
{
return RedirectToAction("Edit", new { id });
}
var providerOrganization = await _providerOrganizationRepository.GetByOrganizationId(id);
if (providerOrganization is null)
{
return RedirectToAction("Edit", new { id });
}
await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider(
provider,
providerOrganization,
organization);
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
return Json(null);
}
private async Task<Organization> GetOrganization(Guid id, OrganizationEditModel model) private async Task<Organization> GetOrganization(Guid id, OrganizationEditModel model)
{ {
var organization = await _organizationRepository.GetByIdAsync(id); var organization = await _organizationRepository.GetByIdAsync(id);

View File

@ -0,0 +1,67 @@
using Bit.Admin.Enums;
using Bit.Admin.Utilities;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Commands;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Admin.Controllers;
[Authorize]
[SelfHosted(NotSelfHostedOnly = true)]
public class ProviderOrganizationsController : Controller
{
private readonly IProviderRepository _providerRepository;
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
private readonly IRemovePaymentMethodCommand _removePaymentMethodCommand;
public ProviderOrganizationsController(IProviderRepository providerRepository,
IProviderOrganizationRepository providerOrganizationRepository,
IOrganizationRepository organizationRepository,
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
IRemovePaymentMethodCommand removePaymentMethodCommand)
{
_providerRepository = providerRepository;
_providerOrganizationRepository = providerOrganizationRepository;
_organizationRepository = organizationRepository;
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
_removePaymentMethodCommand = removePaymentMethodCommand;
}
[HttpPost]
[RequirePermission(Permission.Provider_Edit)]
public async Task<IActionResult> DeleteAsync(Guid providerId, Guid id)
{
var provider = await _providerRepository.GetByIdAsync(providerId);
if (provider is null)
{
return RedirectToAction("Index", "Providers");
}
var providerOrganization = await _providerOrganizationRepository.GetByIdAsync(id);
if (providerOrganization is null)
{
return RedirectToAction("View", "Providers", new { id = providerId });
}
var organization = await _organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);
if (organization == null)
{
return RedirectToAction("View", "Providers", new { id = providerId });
}
await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider(
provider,
providerOrganization,
organization);
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
return Json(null);
}
}

View File

@ -9,6 +9,7 @@ using Stripe;
using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Bit.Admin.Services; using Bit.Admin.Services;
using Bit.Core.Billing.Extensions;
#if !OSS #if !OSS
using Bit.Commercial.Core.Utilities; using Bit.Commercial.Core.Utilities;
@ -87,6 +88,7 @@ public class Startup
services.AddBaseServices(globalSettings); services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings); services.AddDefaultServices(globalSettings);
services.AddScoped<IAccessControlService, AccessControlService>(); services.AddScoped<IAccessControlService, AccessControlService>();
services.AddBillingCommands();
#if OSS #if OSS
services.AddOosServices(); services.AddOosServices();

View File

@ -8,6 +8,7 @@
var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View); var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View);
var canInitiateTrial = AccessControlService.UserHasPermission(Permission.Org_InitiateTrial); var canInitiateTrial = AccessControlService.UserHasPermission(Permission.Org_InitiateTrial);
var canDelete = AccessControlService.UserHasPermission(Permission.Org_Delete); var canDelete = AccessControlService.UserHasPermission(Permission.Org_Delete);
var canUnlinkFromProvider = AccessControlService.UserHasPermission(Permission.Provider_Edit);
} }
@section Scripts { @section Scripts {
@ -81,7 +82,7 @@
<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>
<div class="ml-auto d-flex"> <div class="ml-auto d-flex">
@if (canInitiateTrial) @if (canInitiateTrial && Model.Provider is null)
{ {
<button class="btn btn-secondary mr-2" type="button" id="teams-trial"> <button class="btn btn-secondary mr-2" type="button" id="teams-trial">
Teams Trial Teams Trial
@ -90,6 +91,15 @@
Enterprise Trial Enterprise Trial
</button> </button>
} }
@if (canUnlinkFromProvider && Model.Provider is not null)
{
<button
class="btn btn-outline-danger mr-2"
onclick="return unlinkProvider('@Model.Organization.Id');"
>
Unlink provider
</button>
}
@if (canDelete) @if (canDelete)
{ {
<form asp-action="Delete" asp-route-id="@Model.Organization.Id" <form asp-action="Delete" asp-route-id="@Model.Organization.Id"

View File

@ -1,7 +1,15 @@
@using Bit.Core.AdminConsole.Enums.Provider @using Bit.Core.AdminConsole.Enums.Provider
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using Bit.Admin.Enums
@inject Bit.Admin.Services.IAccessControlService AccessControlService
@model ProviderViewModel @model ProviderViewModel
@{
var canUnlinkFromProvider = AccessControlService.UserHasPermission(Permission.Provider_Edit);
}
@await Html.PartialAsync("_ProviderScripts") @await Html.PartialAsync("_ProviderScripts")
@await Html.PartialAsync("_ProviderOrganizationScripts")
<h2>Provider Organizations</h2> <h2>Provider Organizations</h2>
<div class="row"> <div class="row">
@ -32,26 +40,28 @@
} }
else else
{ {
@foreach (var org in Model.ProviderOrganizations) @foreach (var providerOrganization in Model.ProviderOrganizations)
{ {
<tr> <tr>
<td class="align-middle"> <td class="align-middle">
<a asp-controller="Organizations" asp-action="Edit" asp-route-id="@org.OrganizationId">@org.OrganizationName</a> <a asp-controller="Organizations" asp-action="Edit" asp-route-id="@providerOrganization.OrganizationId">@providerOrganization.OrganizationName</a>
</td> </td>
<td> <td>
@org.Status @providerOrganization.Status
</td> </td>
<td> <td>
<div class="float-right"> <div class="float-right">
@if (org.Status == OrganizationStatusType.Pending) @if (canUnlinkFromProvider)
{ {
<a href="#" class="float-right" onclick="return resendOwnerInvite('@org.OrganizationId');"> <a href="#" class="text-danger float-right" onclick="return unlinkProvider('@Model.Provider.Id', '@providerOrganization.Id');">
<i class="fa fa-envelope-o fa-lg" title="Resend Setup Invite"></i> Unlink provider
</a> </a>
} }
else @if (providerOrganization.Status == OrganizationStatusType.Pending)
{ {
<i class="fa fa-envelope-o fa-lg text-secondary"></i> <a href="#" class="float-right mr-3" onclick="return resendOwnerInvite('@providerOrganization.OrganizationId');">
Resend invitation
</a>
} }
</div> </div>
</td> </td>

View File

@ -0,0 +1,21 @@
<script>
function unlinkProvider(providerId, id) {
if (confirm('Are you sure you want to unlink this organization from its provider?')) {
$.ajax({
type: "POST",
url: `@Url.Action("Delete", "ProviderOrganizations")?providerId=${providerId}&id=${id}`,
dataType: 'json',
contentType: false,
processData: false,
success: function (response) {
alert("Successfully unlinked provider");
window.location.href = `@Url.Action("Edit", "Providers")?id=${providerId}`;
},
error: function (response) {
alert("Error!");
}
});
}
return false;
}
</script>

View File

@ -276,20 +276,7 @@
<div class="col-sm"> <div class="col-sm">
<div class="form-group"> <div class="form-group">
<label asp-for="BillingEmail"></label> <label asp-for="BillingEmail"></label>
@if (Model.Provider?.Type == ProviderType.Reseller) <input type="email" class="form-control" asp-for="BillingEmail" readonly="readonly">
{
<input type="email" class="form-control" asp-for="BillingEmail" readonly="readonly">
}
else
{
<input
type="text"
class="form-control"
asp-for="BillingEmail"
readonly='@(!canEditBilling)'
pattern="@(@"[^@\s]+@[^@\s]+\.[^@\s]+")"
title="Email address must be in the format 'address@domain.com'.">
}
</div> </div>
</div> </div>
<div class="col-sm"> <div class="col-sm">

View File

@ -113,6 +113,26 @@
} }
} }
function unlinkProvider(id) {
if (confirm('Are you sure you want to unlink this organization from its provider?')) {
$.ajax({
type: "POST",
url: `@Url.Action("UnlinkOrganizationFromProvider", "Organizations")?id=${id}`,
dataType: 'json',
contentType: false,
processData: false,
success: function (response) {
alert("Successfully unlinked provider");
window.location.href = `@Url.Action("Edit", "Organizations")?id=${id}`;
},
error: function (response) {
alert("Error!");
}
});
}
return false;
}
/*** /***
* Set Secrets Manager values based on current usage (for migrating from SM beta or reinstating an old subscription) * Set Secrets Manager values based on current usage (for migrating from SM beta or reinstating an old subscription)
*/ */

View File

@ -3,7 +3,6 @@ using Bit.Api.AdminConsole.Models.Response;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.Utilities; using Bit.Api.Utilities;
using Bit.Api.Vault.AuthorizationHandlers.Groups; using Bit.Api.Vault.AuthorizationHandlers.Groups;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
@ -27,10 +26,8 @@ public class GroupsController : Controller
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly ICreateGroupCommand _createGroupCommand; private readonly ICreateGroupCommand _createGroupCommand;
private readonly IUpdateGroupCommand _updateGroupCommand; private readonly IUpdateGroupCommand _updateGroupCommand;
private readonly IFeatureService _featureService;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly IApplicationCacheService _applicationCacheService;
private bool UseFlexibleCollections => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
public GroupsController( public GroupsController(
IGroupRepository groupRepository, IGroupRepository groupRepository,
@ -41,7 +38,8 @@ public class GroupsController : Controller
IUpdateGroupCommand updateGroupCommand, IUpdateGroupCommand updateGroupCommand,
IDeleteGroupCommand deleteGroupCommand, IDeleteGroupCommand deleteGroupCommand,
IFeatureService featureService, IFeatureService featureService,
IAuthorizationService authorizationService) IAuthorizationService authorizationService,
IApplicationCacheService applicationCacheService)
{ {
_groupRepository = groupRepository; _groupRepository = groupRepository;
_groupService = groupService; _groupService = groupService;
@ -50,8 +48,8 @@ public class GroupsController : Controller
_createGroupCommand = createGroupCommand; _createGroupCommand = createGroupCommand;
_updateGroupCommand = updateGroupCommand; _updateGroupCommand = updateGroupCommand;
_deleteGroupCommand = deleteGroupCommand; _deleteGroupCommand = deleteGroupCommand;
_featureService = featureService;
_authorizationService = authorizationService; _authorizationService = authorizationService;
_applicationCacheService = applicationCacheService;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
@ -81,7 +79,7 @@ public class GroupsController : Controller
[HttpGet("")] [HttpGet("")]
public async Task<ListResponseModel<GroupDetailsResponseModel>> Get(Guid orgId) public async Task<ListResponseModel<GroupDetailsResponseModel>> Get(Guid orgId)
{ {
if (UseFlexibleCollections) if (await FlexibleCollectionsIsEnabledAsync(orgId))
{ {
// New flexible collections logic // New flexible collections logic
return await Get_vNext(orgId); return await Get_vNext(orgId);
@ -217,4 +215,10 @@ public class GroupsController : Controller
var responses = groups.Select(g => new GroupDetailsResponseModel(g.Item1, g.Item2)); var responses = groups.Select(g => new GroupDetailsResponseModel(g.Item1, g.Item2));
return new ListResponseModel<GroupDetailsResponseModel>(responses); return new ListResponseModel<GroupDetailsResponseModel>(responses);
} }
private async Task<bool> FlexibleCollectionsIsEnabledAsync(Guid organizationId)
{
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
return organizationAbility?.FlexibleCollections ?? false;
}
} }

View File

@ -4,7 +4,6 @@ using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.Utilities; using Bit.Api.Utilities;
using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers; using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers;
using Bit.Core;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
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;
@ -39,10 +38,8 @@ public class OrganizationUsersController : Controller
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand; private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand;
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand; private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
private readonly IFeatureService _featureService;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly IApplicationCacheService _applicationCacheService;
private bool UseFlexibleCollections => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
public OrganizationUsersController( public OrganizationUsersController(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
@ -57,8 +54,8 @@ public class OrganizationUsersController : Controller
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand, IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand,
IAcceptOrgUserCommand acceptOrgUserCommand, IAcceptOrgUserCommand acceptOrgUserCommand,
IFeatureService featureService, IAuthorizationService authorizationService,
IAuthorizationService authorizationService) IApplicationCacheService applicationCacheService)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -72,8 +69,8 @@ public class OrganizationUsersController : Controller
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
_updateOrganizationUserGroupsCommand = updateOrganizationUserGroupsCommand; _updateOrganizationUserGroupsCommand = updateOrganizationUserGroupsCommand;
_acceptOrgUserCommand = acceptOrgUserCommand; _acceptOrgUserCommand = acceptOrgUserCommand;
_featureService = featureService;
_authorizationService = authorizationService; _authorizationService = authorizationService;
_applicationCacheService = applicationCacheService;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
@ -98,7 +95,7 @@ public class OrganizationUsersController : Controller
[HttpGet("")] [HttpGet("")]
public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false) public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false)
{ {
var authorized = UseFlexibleCollections var authorized = await FlexibleCollectionsIsEnabledAsync(orgId)
? (await _authorizationService.AuthorizeAsync(User, OrganizationUserOperations.ReadAll(orgId))).Succeeded ? (await _authorizationService.AuthorizeAsync(User, OrganizationUserOperations.ReadAll(orgId))).Succeeded
: await _currentContext.ViewAllCollections(orgId) || : await _currentContext.ViewAllCollections(orgId) ||
await _currentContext.ViewAssignedCollections(orgId) || await _currentContext.ViewAssignedCollections(orgId) ||
@ -518,4 +515,10 @@ public class OrganizationUsersController : Controller
return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r => return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r =>
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2))); new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
} }
private async Task<bool> FlexibleCollectionsIsEnabledAsync(Guid organizationId)
{
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
return organizationAbility?.FlexibleCollections ?? false;
}
} }

View File

@ -40,7 +40,6 @@ public class OrganizationsController : Controller
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly IProviderRepository _providerRepository;
private readonly IOrganizationService _organizationService; private readonly IOrganizationService _organizationService;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IPaymentService _paymentService; private readonly IPaymentService _paymentService;
@ -51,7 +50,6 @@ 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 IUpdateOrganizationLicenseCommand _updateOrganizationLicenseCommand;
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery; private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
@ -64,7 +62,6 @@ public class OrganizationsController : Controller
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
IProviderRepository providerRepository,
IOrganizationService organizationService, IOrganizationService organizationService,
IUserService userService, IUserService userService,
IPaymentService paymentService, IPaymentService paymentService,
@ -75,7 +72,6 @@ public class OrganizationsController : Controller
IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand, IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand,
ICreateOrganizationApiKeyCommand createOrganizationApiKeyCommand, ICreateOrganizationApiKeyCommand createOrganizationApiKeyCommand,
IOrganizationApiKeyRepository organizationApiKeyRepository, IOrganizationApiKeyRepository organizationApiKeyRepository,
IUpdateOrganizationLicenseCommand updateOrganizationLicenseCommand,
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery, ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
IFeatureService featureService, IFeatureService featureService,
GlobalSettings globalSettings, GlobalSettings globalSettings,
@ -87,7 +83,6 @@ public class OrganizationsController : Controller
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_policyRepository = policyRepository; _policyRepository = policyRepository;
_providerRepository = providerRepository;
_organizationService = organizationService; _organizationService = organizationService;
_userService = userService; _userService = userService;
_paymentService = paymentService; _paymentService = paymentService;
@ -98,7 +93,6 @@ public class OrganizationsController : Controller
_rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand; _rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand;
_createOrganizationApiKeyCommand = createOrganizationApiKeyCommand; _createOrganizationApiKeyCommand = createOrganizationApiKeyCommand;
_organizationApiKeyRepository = organizationApiKeyRepository; _organizationApiKeyRepository = organizationApiKeyRepository;
_updateOrganizationLicenseCommand = updateOrganizationLicenseCommand;
_cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery; _cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery;
_featureService = featureService; _featureService = featureService;
_globalSettings = globalSettings; _globalSettings = globalSettings;
@ -245,6 +239,21 @@ public class OrganizationsController : Controller
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false); return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false);
} }
[HttpGet("{id}/risks-subscription-failure")]
public async Task<OrganizationRisksSubscriptionFailureResponseModel> RisksSubscriptionFailure(Guid id)
{
if (!await _currentContext.EditPaymentMethods(id))
{
return new OrganizationRisksSubscriptionFailureResponseModel(id, false);
}
var organization = await _organizationRepository.GetByIdAsync(id);
var risksSubscriptionFailure = await _paymentService.RisksSubscriptionFailure(organization);
return new OrganizationRisksSubscriptionFailureResponseModel(id, 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)
@ -775,7 +784,6 @@ public class OrganizationsController : Controller
} }
[HttpPut("{id}/collection-management")] [HttpPut("{id}/collection-management")]
[RequireFeature(FeatureFlagKeys.FlexibleCollections)]
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
public async Task<OrganizationResponseModel> PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model) public async Task<OrganizationResponseModel> PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model)
{ {
@ -790,6 +798,11 @@ public class OrganizationsController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
if (!organization.FlexibleCollections)
{
throw new BadRequestException("Organization does not have collection enhancements enabled");
}
var v1Enabled = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1, _currentContext); var v1Enabled = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1, _currentContext);
if (!v1Enabled) if (!v1Enabled)

View File

@ -1,10 +1,13 @@
using Bit.Api.AdminConsole.Models.Request.Providers; using Bit.Api.AdminConsole.Models.Request.Providers;
using Bit.Api.AdminConsole.Models.Response.Providers; using Bit.Api.AdminConsole.Models.Response.Providers;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
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.Context; using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -16,22 +19,33 @@ namespace Bit.Api.AdminConsole.Controllers;
[Authorize("Application")] [Authorize("Application")]
public class ProviderOrganizationsController : Controller public class ProviderOrganizationsController : Controller
{ {
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IProviderService _providerService;
private readonly IUserService _userService;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IOrganizationRepository _organizationRepository;
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IProviderRepository _providerRepository;
private readonly IProviderService _providerService;
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
private readonly IRemovePaymentMethodCommand _removePaymentMethodCommand;
private readonly IUserService _userService;
public ProviderOrganizationsController( public ProviderOrganizationsController(
ICurrentContext currentContext,
IOrganizationRepository organizationRepository,
IProviderOrganizationRepository providerOrganizationRepository, IProviderOrganizationRepository providerOrganizationRepository,
IProviderRepository providerRepository,
IProviderService providerService, IProviderService providerService,
IUserService userService, IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
ICurrentContext currentContext) IRemovePaymentMethodCommand removePaymentMethodCommand,
IUserService userService)
{ {
_providerOrganizationRepository = providerOrganizationRepository;
_providerService = providerService;
_userService = userService;
_currentContext = currentContext; _currentContext = currentContext;
_organizationRepository = organizationRepository;
_providerOrganizationRepository = providerOrganizationRepository;
_providerRepository = providerRepository;
_providerService = providerService;
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
_removePaymentMethodCommand = removePaymentMethodCommand;
_userService = userService;
} }
[HttpGet("")] [HttpGet("")]
@ -87,7 +101,17 @@ public class ProviderOrganizationsController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
var userId = _userService.GetProperUserId(User); var provider = await _providerRepository.GetByIdAsync(providerId);
await _providerService.RemoveOrganizationAsync(providerId, id, userId.Value);
var providerOrganization = await _providerOrganizationRepository.GetByIdAsync(id);
var organization = await _organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);
await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider(
provider,
providerOrganization,
organization);
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
} }
} }

View File

@ -0,0 +1,17 @@
using Bit.Core.Models.Api;
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
public class OrganizationRisksSubscriptionFailureResponseModel : ResponseModel
{
public Guid OrganizationId { get; }
public bool RisksSubscriptionFailure { get; }
public OrganizationRisksSubscriptionFailureResponseModel(
Guid organizationId,
bool risksSubscriptionFailure) : base("organizationRisksSubscriptionFailure")
{
OrganizationId = organizationId;
RisksSubscriptionFailure = risksSubscriptionFailure;
}
}

View File

@ -28,8 +28,8 @@ public class CollectionsController : Controller
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IBulkAddCollectionAccessCommand _bulkAddCollectionAccessCommand; private readonly IBulkAddCollectionAccessCommand _bulkAddCollectionAccessCommand;
private readonly IFeatureService _featureService;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IApplicationCacheService _applicationCacheService;
public CollectionsController( public CollectionsController(
ICollectionRepository collectionRepository, ICollectionRepository collectionRepository,
@ -39,8 +39,8 @@ public class CollectionsController : Controller
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
ICurrentContext currentContext, ICurrentContext currentContext,
IBulkAddCollectionAccessCommand bulkAddCollectionAccessCommand, IBulkAddCollectionAccessCommand bulkAddCollectionAccessCommand,
IFeatureService featureService, IOrganizationUserRepository organizationUserRepository,
IOrganizationUserRepository organizationUserRepository) IApplicationCacheService applicationCacheService)
{ {
_collectionRepository = collectionRepository; _collectionRepository = collectionRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -50,16 +50,14 @@ public class CollectionsController : Controller
_authorizationService = authorizationService; _authorizationService = authorizationService;
_currentContext = currentContext; _currentContext = currentContext;
_bulkAddCollectionAccessCommand = bulkAddCollectionAccessCommand; _bulkAddCollectionAccessCommand = bulkAddCollectionAccessCommand;
_featureService = featureService;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_applicationCacheService = applicationCacheService;
} }
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<CollectionResponseModel> Get(Guid orgId, Guid id) public async Task<CollectionResponseModel> Get(Guid orgId, Guid id)
{ {
if (FlexibleCollectionsIsEnabled) if (await FlexibleCollectionsIsEnabledAsync(orgId))
{ {
// New flexible collections logic // New flexible collections logic
return await Get_vNext(id); return await Get_vNext(id);
@ -79,7 +77,7 @@ public class CollectionsController : Controller
[HttpGet("{id}/details")] [HttpGet("{id}/details")]
public async Task<CollectionAccessDetailsResponseModel> GetDetails(Guid orgId, Guid id) public async Task<CollectionAccessDetailsResponseModel> GetDetails(Guid orgId, Guid id)
{ {
if (FlexibleCollectionsIsEnabled) if (await FlexibleCollectionsIsEnabledAsync(orgId))
{ {
// New flexible collections logic // New flexible collections logic
return await GetDetails_vNext(id); return await GetDetails_vNext(id);
@ -104,7 +102,7 @@ public class CollectionsController : Controller
else else
{ {
(var collection, var access) = await _collectionRepository.GetByIdWithAccessAsync(id, (var collection, var access) = await _collectionRepository.GetByIdWithAccessAsync(id,
_currentContext.UserId.Value, FlexibleCollectionsIsEnabled); _currentContext.UserId.Value, false);
if (collection == null || collection.OrganizationId != orgId) if (collection == null || collection.OrganizationId != orgId)
{ {
throw new NotFoundException(); throw new NotFoundException();
@ -117,7 +115,7 @@ public class CollectionsController : Controller
[HttpGet("details")] [HttpGet("details")]
public async Task<ListResponseModel<CollectionAccessDetailsResponseModel>> GetManyWithDetails(Guid orgId) public async Task<ListResponseModel<CollectionAccessDetailsResponseModel>> GetManyWithDetails(Guid orgId)
{ {
if (FlexibleCollectionsIsEnabled) if (await FlexibleCollectionsIsEnabledAsync(orgId))
{ {
// New flexible collections logic // New flexible collections logic
return await GetManyWithDetails_vNext(orgId); return await GetManyWithDetails_vNext(orgId);
@ -132,7 +130,7 @@ public class CollectionsController : Controller
// We always need to know which collections the current user is assigned to // We always need to know which collections the current user is assigned to
var assignedOrgCollections = var assignedOrgCollections =
await _collectionRepository.GetManyByUserIdWithAccessAsync(_currentContext.UserId.Value, orgId, await _collectionRepository.GetManyByUserIdWithAccessAsync(_currentContext.UserId.Value, orgId,
FlexibleCollectionsIsEnabled); false);
if (await _currentContext.ViewAllCollections(orgId) || await _currentContext.ManageUsers(orgId)) if (await _currentContext.ViewAllCollections(orgId) || await _currentContext.ManageUsers(orgId))
{ {
@ -159,7 +157,7 @@ public class CollectionsController : Controller
[HttpGet("")] [HttpGet("")]
public async Task<ListResponseModel<CollectionResponseModel>> Get(Guid orgId) public async Task<ListResponseModel<CollectionResponseModel>> Get(Guid orgId)
{ {
if (FlexibleCollectionsIsEnabled) if (await FlexibleCollectionsIsEnabledAsync(orgId))
{ {
// New flexible collections logic // New flexible collections logic
return await GetByOrgId_vNext(orgId); return await GetByOrgId_vNext(orgId);
@ -191,7 +189,7 @@ public class CollectionsController : Controller
public async Task<ListResponseModel<CollectionDetailsResponseModel>> GetUser() public async Task<ListResponseModel<CollectionDetailsResponseModel>> GetUser()
{ {
var collections = await _collectionRepository.GetManyByUserIdAsync( var collections = await _collectionRepository.GetManyByUserIdAsync(
_userService.GetProperUserId(User).Value, FlexibleCollectionsIsEnabled); _userService.GetProperUserId(User).Value, false);
var responses = collections.Select(c => new CollectionDetailsResponseModel(c)); var responses = collections.Select(c => new CollectionDetailsResponseModel(c));
return new ListResponseModel<CollectionDetailsResponseModel>(responses); return new ListResponseModel<CollectionDetailsResponseModel>(responses);
} }
@ -199,7 +197,7 @@ public class CollectionsController : Controller
[HttpGet("{id}/users")] [HttpGet("{id}/users")]
public async Task<IEnumerable<SelectionReadOnlyResponseModel>> GetUsers(Guid orgId, Guid id) public async Task<IEnumerable<SelectionReadOnlyResponseModel>> GetUsers(Guid orgId, Guid id)
{ {
if (FlexibleCollectionsIsEnabled) if (await FlexibleCollectionsIsEnabledAsync(orgId))
{ {
// New flexible collections logic // New flexible collections logic
return await GetUsers_vNext(id); return await GetUsers_vNext(id);
@ -217,7 +215,8 @@ public class CollectionsController : Controller
{ {
var collection = model.ToCollection(orgId); var collection = model.ToCollection(orgId);
var authorized = FlexibleCollectionsIsEnabled var flexibleCollectionsIsEnabled = await FlexibleCollectionsIsEnabledAsync(orgId);
var authorized = flexibleCollectionsIsEnabled
? (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Create)).Succeeded ? (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Create)).Succeeded
: await CanCreateCollection(orgId, collection.Id) || await CanEditCollectionAsync(orgId, collection.Id); : await CanCreateCollection(orgId, collection.Id) || await CanEditCollectionAsync(orgId, collection.Id);
if (!authorized) if (!authorized)
@ -230,7 +229,7 @@ public class CollectionsController : Controller
// Pre-flexible collections logic assigned Managers to collections they create // Pre-flexible collections logic assigned Managers to collections they create
var assignUserToCollection = var assignUserToCollection =
!FlexibleCollectionsIsEnabled && !flexibleCollectionsIsEnabled &&
!await _currentContext.EditAnyCollection(orgId) && !await _currentContext.EditAnyCollection(orgId) &&
await _currentContext.EditAssignedCollections(orgId); await _currentContext.EditAssignedCollections(orgId);
var isNewCollection = collection.Id == default; var isNewCollection = collection.Id == default;
@ -258,7 +257,7 @@ public class CollectionsController : Controller
[HttpPost("{id}")] [HttpPost("{id}")]
public async Task<CollectionResponseModel> Put(Guid orgId, Guid id, [FromBody] CollectionRequestModel model) public async Task<CollectionResponseModel> Put(Guid orgId, Guid id, [FromBody] CollectionRequestModel model)
{ {
if (FlexibleCollectionsIsEnabled) if (await FlexibleCollectionsIsEnabledAsync(orgId))
{ {
// New flexible collections logic // New flexible collections logic
return await Put_vNext(id, model); return await Put_vNext(id, model);
@ -280,7 +279,7 @@ public class CollectionsController : Controller
[HttpPut("{id}/users")] [HttpPut("{id}/users")]
public async Task PutUsers(Guid orgId, Guid id, [FromBody] IEnumerable<SelectionReadOnlyRequestModel> model) public async Task PutUsers(Guid orgId, Guid id, [FromBody] IEnumerable<SelectionReadOnlyRequestModel> model)
{ {
if (FlexibleCollectionsIsEnabled) if (await FlexibleCollectionsIsEnabledAsync(orgId))
{ {
// New flexible collections logic // New flexible collections logic
await PutUsers_vNext(id, model); await PutUsers_vNext(id, model);
@ -299,14 +298,17 @@ public class CollectionsController : Controller
[HttpPost("bulk-access")] [HttpPost("bulk-access")]
[RequireFeature(FeatureFlagKeys.BulkCollectionAccess)] [RequireFeature(FeatureFlagKeys.BulkCollectionAccess)]
// Also gated behind Flexible Collections flag because it only has new authorization logic. public async Task PostBulkCollectionAccess(Guid orgId, [FromBody] BulkCollectionAccessRequestModel model)
// Could be removed if legacy authorization logic were implemented for many collections.
[RequireFeature(FeatureFlagKeys.FlexibleCollections)]
public async Task PostBulkCollectionAccess([FromBody] BulkCollectionAccessRequestModel model)
{ {
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.CollectionIds); // Authorization logic assumes flexible collections is enabled
// Remove after all organizations have been migrated
if (!await FlexibleCollectionsIsEnabledAsync(orgId))
{
throw new NotFoundException("Feature disabled.");
}
if (collections.Count != model.CollectionIds.Count()) var collections = await _collectionRepository.GetManyByManyIdsAsync(model.CollectionIds);
if (collections.Count(c => c.OrganizationId == orgId) != model.CollectionIds.Count())
{ {
throw new NotFoundException("One or more collections not found."); throw new NotFoundException("One or more collections not found.");
} }
@ -328,7 +330,7 @@ public class CollectionsController : Controller
[HttpPost("{id}/delete")] [HttpPost("{id}/delete")]
public async Task Delete(Guid orgId, Guid id) public async Task Delete(Guid orgId, Guid id)
{ {
if (FlexibleCollectionsIsEnabled) if (await FlexibleCollectionsIsEnabledAsync(orgId))
{ {
// New flexible collections logic // New flexible collections logic
await Delete_vNext(id); await Delete_vNext(id);
@ -349,7 +351,7 @@ public class CollectionsController : Controller
[HttpPost("delete")] [HttpPost("delete")]
public async Task DeleteMany(Guid orgId, [FromBody] CollectionBulkDeleteRequestModel model) public async Task DeleteMany(Guid orgId, [FromBody] CollectionBulkDeleteRequestModel model)
{ {
if (FlexibleCollectionsIsEnabled) if (await FlexibleCollectionsIsEnabledAsync(orgId))
{ {
// New flexible collections logic // New flexible collections logic
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Ids); var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Ids);
@ -385,7 +387,7 @@ public class CollectionsController : Controller
[HttpPost("{id}/delete-user/{orgUserId}")] [HttpPost("{id}/delete-user/{orgUserId}")]
public async Task DeleteUser(Guid orgId, Guid id, Guid orgUserId) public async Task DeleteUser(Guid orgId, Guid id, Guid orgUserId)
{ {
if (FlexibleCollectionsIsEnabled) if (await FlexibleCollectionsIsEnabledAsync(orgId))
{ {
// New flexible collections logic // New flexible collections logic
await DeleteUser_vNext(id, orgUserId); await DeleteUser_vNext(id, orgUserId);
@ -397,19 +399,9 @@ public class CollectionsController : Controller
await _collectionService.DeleteUserAsync(collection, orgUserId); await _collectionService.DeleteUserAsync(collection, orgUserId);
} }
private void DeprecatedPermissionsGuard()
{
if (FlexibleCollectionsIsEnabled)
{
throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF.");
}
}
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")] [Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
private async Task<Collection> GetCollectionAsync(Guid id, Guid orgId) private async Task<Collection> GetCollectionAsync(Guid id, Guid orgId)
{ {
DeprecatedPermissionsGuard();
Collection collection = default; Collection collection = default;
if (await _currentContext.ViewAllCollections(orgId)) if (await _currentContext.ViewAllCollections(orgId))
{ {
@ -417,7 +409,7 @@ public class CollectionsController : Controller
} }
else if (await _currentContext.ViewAssignedCollections(orgId)) else if (await _currentContext.ViewAssignedCollections(orgId))
{ {
collection = await _collectionRepository.GetByIdAsync(id, _currentContext.UserId.Value, FlexibleCollectionsIsEnabled); collection = await _collectionRepository.GetByIdAsync(id, _currentContext.UserId.Value, false);
} }
if (collection == null || collection.OrganizationId != orgId) if (collection == null || collection.OrganizationId != orgId)
@ -431,8 +423,6 @@ public class CollectionsController : Controller
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")] [Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
private async Task<bool> CanCreateCollection(Guid orgId, Guid collectionId) private async Task<bool> CanCreateCollection(Guid orgId, Guid collectionId)
{ {
DeprecatedPermissionsGuard();
if (collectionId != default) if (collectionId != default)
{ {
return false; return false;
@ -445,8 +435,6 @@ public class CollectionsController : Controller
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")] [Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
private async Task<bool> CanEditCollectionAsync(Guid orgId, Guid collectionId) private async Task<bool> CanEditCollectionAsync(Guid orgId, Guid collectionId)
{ {
DeprecatedPermissionsGuard();
if (collectionId == default) if (collectionId == default)
{ {
return false; return false;
@ -460,7 +448,7 @@ public class CollectionsController : Controller
if (await _currentContext.EditAssignedCollections(orgId)) if (await _currentContext.EditAssignedCollections(orgId))
{ {
var collectionDetails = var collectionDetails =
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value, FlexibleCollectionsIsEnabled); await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value, false);
return collectionDetails != null; return collectionDetails != null;
} }
@ -470,8 +458,6 @@ public class CollectionsController : Controller
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")] [Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
private async Task<bool> CanDeleteCollectionAsync(Guid orgId, Guid collectionId) private async Task<bool> CanDeleteCollectionAsync(Guid orgId, Guid collectionId)
{ {
DeprecatedPermissionsGuard();
if (collectionId == default) if (collectionId == default)
{ {
return false; return false;
@ -485,7 +471,7 @@ public class CollectionsController : Controller
if (await _currentContext.DeleteAssignedCollections(orgId)) if (await _currentContext.DeleteAssignedCollections(orgId))
{ {
var collectionDetails = var collectionDetails =
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value, FlexibleCollectionsIsEnabled); await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value, false);
return collectionDetails != null; return collectionDetails != null;
} }
@ -495,8 +481,6 @@ public class CollectionsController : Controller
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")] [Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
private async Task<bool> DeleteAnyCollection(Guid orgId) private async Task<bool> DeleteAnyCollection(Guid orgId)
{ {
DeprecatedPermissionsGuard();
return await _currentContext.OrganizationAdmin(orgId) || return await _currentContext.OrganizationAdmin(orgId) ||
(_currentContext.Organizations?.Any(o => o.Id == orgId (_currentContext.Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.DeleteAnyCollection ?? false)) ?? false); && (o.Permissions?.DeleteAnyCollection ?? false)) ?? false);
@ -505,8 +489,6 @@ public class CollectionsController : Controller
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")] [Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
private async Task<bool> CanViewCollectionAsync(Guid orgId, Guid collectionId) private async Task<bool> CanViewCollectionAsync(Guid orgId, Guid collectionId)
{ {
DeprecatedPermissionsGuard();
if (collectionId == default) if (collectionId == default)
{ {
return false; return false;
@ -520,7 +502,7 @@ public class CollectionsController : Controller
if (await _currentContext.ViewAssignedCollections(orgId)) if (await _currentContext.ViewAssignedCollections(orgId))
{ {
var collectionDetails = var collectionDetails =
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value, FlexibleCollectionsIsEnabled); await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value, false);
return collectionDetails != null; return collectionDetails != null;
} }
@ -530,8 +512,6 @@ public class CollectionsController : Controller
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")] [Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
private async Task<bool> ViewAtLeastOneCollectionAsync(Guid orgId) private async Task<bool> ViewAtLeastOneCollectionAsync(Guid orgId)
{ {
DeprecatedPermissionsGuard();
return await _currentContext.ViewAllCollections(orgId) || await _currentContext.ViewAssignedCollections(orgId); return await _currentContext.ViewAllCollections(orgId) || await _currentContext.ViewAssignedCollections(orgId);
} }
@ -564,7 +544,7 @@ public class CollectionsController : Controller
{ {
// We always need to know which collections the current user is assigned to // We always need to know which collections the current user is assigned to
var assignedOrgCollections = await _collectionRepository var assignedOrgCollections = await _collectionRepository
.GetManyByUserIdWithAccessAsync(_currentContext.UserId.Value, orgId, FlexibleCollectionsIsEnabled); .GetManyByUserIdWithAccessAsync(_currentContext.UserId.Value, orgId, false);
var readAllAuthorized = var readAllAuthorized =
(await _authorizationService.AuthorizeAsync(User, CollectionOperations.ReadAllWithAccess(orgId))).Succeeded; (await _authorizationService.AuthorizeAsync(User, CollectionOperations.ReadAllWithAccess(orgId))).Succeeded;
@ -604,7 +584,7 @@ public class CollectionsController : Controller
} }
else else
{ {
var assignedCollections = await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId.Value, FlexibleCollectionsIsEnabled); var assignedCollections = await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId.Value, false);
orgCollections = assignedCollections.Where(c => c.OrganizationId == orgId && c.Manage).ToList(); orgCollections = assignedCollections.Where(c => c.OrganizationId == orgId && c.Manage).ToList();
} }
@ -676,4 +656,10 @@ public class CollectionsController : Controller
await _collectionService.DeleteUserAsync(collection, orgUserId); await _collectionService.DeleteUserAsync(collection, orgUserId);
} }
private async Task<bool> FlexibleCollectionsIsEnabledAsync(Guid organizationId)
{
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
return organizationAbility?.FlexibleCollections ?? false;
}
} }

View File

@ -26,6 +26,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using Bit.Core.Auth.Identity; using Bit.Core.Auth.Identity;
using Bit.Core.Auth.UserFeatures; using Bit.Core.Auth.UserFeatures;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Billing.Extensions;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
using Bit.Core.Tools.Entities; using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities; using Bit.Core.Vault.Entities;
@ -169,6 +170,7 @@ public class Startup
services.AddDefaultServices(globalSettings); services.AddDefaultServices(globalSettings);
services.AddOrganizationSubscriptionServices(); services.AddOrganizationSubscriptionServices();
services.AddCoreLocalizationServices(); services.AddCoreLocalizationServices();
services.AddBillingCommands();
// Authorization Handlers // Authorization Handlers
services.AddAuthorizationHandlers(); services.AddAuthorizationHandlers();

View File

@ -1,4 +1,5 @@
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.Tools.Models.Response;
using Bit.Api.Vault.Models.Response; using Bit.Api.Vault.Models.Response;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
@ -9,7 +10,7 @@ using Bit.Core.Vault.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Controllers; namespace Bit.Api.Tools.Controllers;
[Route("organizations/{organizationId}")] [Route("organizations/{organizationId}")]
[Authorize("Application")] [Authorize("Application")]

View File

@ -1,7 +1,8 @@
using Bit.Api.Vault.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.Vault.Models.Response;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
namespace Bit.Api.Models.Response; namespace Bit.Api.Tools.Models.Response;
public class OrganizationExportResponseModel : ResponseModel public class OrganizationExportResponseModel : ResponseModel
{ {

View File

@ -1,5 +1,4 @@
#nullable enable #nullable enable
using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -20,33 +19,22 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
{ {
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly ICollectionRepository _collectionRepository; private readonly ICollectionRepository _collectionRepository;
private readonly IFeatureService _featureService;
private readonly IApplicationCacheService _applicationCacheService; private readonly IApplicationCacheService _applicationCacheService;
private Guid _targetOrganizationId; private Guid _targetOrganizationId;
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
public BulkCollectionAuthorizationHandler( public BulkCollectionAuthorizationHandler(
ICurrentContext currentContext, ICurrentContext currentContext,
ICollectionRepository collectionRepository, ICollectionRepository collectionRepository,
IFeatureService featureService,
IApplicationCacheService applicationCacheService) IApplicationCacheService applicationCacheService)
{ {
_currentContext = currentContext; _currentContext = currentContext;
_collectionRepository = collectionRepository; _collectionRepository = collectionRepository;
_featureService = featureService;
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
} }
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
BulkCollectionOperationRequirement requirement, ICollection<Collection>? resources) BulkCollectionOperationRequirement requirement, ICollection<Collection>? resources)
{ {
if (!FlexibleCollectionsIsEnabled)
{
// Flexible collections is OFF, should not be using this handler
throw new FeatureUnavailableException("Flexible collections is OFF when it should be ON.");
}
// Establish pattern of authorization handler null checking passed resources // Establish pattern of authorization handler null checking passed resources
if (resources == null || !resources.Any()) if (resources == null || !resources.Any())
{ {
@ -281,9 +269,6 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
return null; return null;
} }
(await _applicationCacheService.GetOrganizationAbilitiesAsync()) return await _applicationCacheService.GetOrganizationAbilityAsync(organization.Id);
.TryGetValue(organization.Id, out var organizationAbility);
return organizationAbility;
} }
} }

View File

@ -1,8 +1,6 @@
#nullable enable #nullable enable
using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Services; using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -17,8 +15,6 @@ public class CollectionAuthorizationHandler : AuthorizationHandler<CollectionOpe
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
public CollectionAuthorizationHandler( public CollectionAuthorizationHandler(
ICurrentContext currentContext, ICurrentContext currentContext,
IFeatureService featureService) IFeatureService featureService)
@ -30,12 +26,6 @@ public class CollectionAuthorizationHandler : AuthorizationHandler<CollectionOpe
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
CollectionOperationRequirement requirement) CollectionOperationRequirement requirement)
{ {
if (!FlexibleCollectionsIsEnabled)
{
// Flexible collections is OFF, should not be using this handler
throw new FeatureUnavailableException("Flexible collections is OFF when it should be ON.");
}
// Acting user is not authenticated, fail // Acting user is not authenticated, fail
if (!_currentContext.UserId.HasValue) if (!_currentContext.UserId.HasValue)
{ {

View File

@ -1,8 +1,6 @@
#nullable enable #nullable enable
using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations;
using Bit.Core.Services; using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -19,8 +17,6 @@ public class GroupAuthorizationHandler : AuthorizationHandler<GroupOperationRequ
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IApplicationCacheService _applicationCacheService; private readonly IApplicationCacheService _applicationCacheService;
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
public GroupAuthorizationHandler( public GroupAuthorizationHandler(
ICurrentContext currentContext, ICurrentContext currentContext,
IFeatureService featureService, IFeatureService featureService,
@ -34,12 +30,6 @@ public class GroupAuthorizationHandler : AuthorizationHandler<GroupOperationRequ
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
GroupOperationRequirement requirement) GroupOperationRequirement requirement)
{ {
if (!FlexibleCollectionsIsEnabled)
{
// Flexible collections is OFF, should not be using this handler
throw new FeatureUnavailableException("Flexible collections is OFF when it should be ON.");
}
// Acting user is not authenticated, fail // Acting user is not authenticated, fail
if (!_currentContext.UserId.HasValue) if (!_currentContext.UserId.HasValue)
{ {
@ -103,9 +93,6 @@ public class GroupAuthorizationHandler : AuthorizationHandler<GroupOperationRequ
return null; return null;
} }
(await _applicationCacheService.GetOrganizationAbilitiesAsync()) return await _applicationCacheService.GetOrganizationAbilityAsync(organization.Id);
.TryGetValue(organization.Id, out var organizationAbility);
return organizationAbility;
} }
} }

View File

@ -1,8 +1,6 @@
#nullable enable #nullable enable
using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations;
using Bit.Core.Services; using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -19,8 +17,6 @@ public class OrganizationUserAuthorizationHandler : AuthorizationHandler<Organiz
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IApplicationCacheService _applicationCacheService; private readonly IApplicationCacheService _applicationCacheService;
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
public OrganizationUserAuthorizationHandler( public OrganizationUserAuthorizationHandler(
ICurrentContext currentContext, ICurrentContext currentContext,
IFeatureService featureService, IFeatureService featureService,
@ -34,12 +30,6 @@ public class OrganizationUserAuthorizationHandler : AuthorizationHandler<Organiz
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
OrganizationUserOperationRequirement requirement) OrganizationUserOperationRequirement requirement)
{ {
if (!FlexibleCollectionsIsEnabled)
{
// Flexible collections is OFF, should not be using this handler
throw new FeatureUnavailableException("Flexible collections is OFF when it should be ON.");
}
if (!_currentContext.UserId.HasValue) if (!_currentContext.UserId.HasValue)
{ {
context.Fail(); context.Fail();
@ -103,9 +93,6 @@ public class OrganizationUserAuthorizationHandler : AuthorizationHandler<Organiz
return null; return null;
} }
(await _applicationCacheService.GetOrganizationAbilitiesAsync()) return await _applicationCacheService.GetOrganizationAbilityAsync(organization.Id);
.TryGetValue(organization.Id, out var organizationAbility);
return organizationAbility;
} }
} }

View File

@ -241,11 +241,13 @@ public class Organization : ITableObject<Guid>, ISubscriber, IStorable, IStorabl
return providers[provider]; return providers[provider];
} }
public void UpdateFromLicense( public void UpdateFromLicense(OrganizationLicense license)
OrganizationLicense license,
bool flexibleCollectionsMvpIsEnabled,
bool flexibleCollectionsV1IsEnabled)
{ {
// The following properties are intentionally excluded from being updated:
// - Id - self-hosted org will have its own unique Guid
// - MaxStorageGb - not enforced for self-hosted because we're not providing the storage
// - FlexibleCollections - the self-hosted organization must do its own data migration to set this property, it cannot be updated from cloud
Name = license.Name; Name = license.Name;
BusinessName = license.BusinessName; BusinessName = license.BusinessName;
BillingEmail = license.BillingEmail; BillingEmail = license.BillingEmail;
@ -275,7 +277,7 @@ public class Organization : ITableObject<Guid>, ISubscriber, IStorable, IStorabl
UseSecretsManager = license.UseSecretsManager; UseSecretsManager = license.UseSecretsManager;
SmSeats = license.SmSeats; SmSeats = license.SmSeats;
SmServiceAccounts = license.SmServiceAccounts; SmServiceAccounts = license.SmServiceAccounts;
LimitCollectionCreationDeletion = !flexibleCollectionsMvpIsEnabled || license.LimitCollectionCreationDeletion; LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = !flexibleCollectionsV1IsEnabled || license.AllowAdminAccessToAllCollectionItems; AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems;
} }
} }

View File

@ -145,7 +145,8 @@ public class SelfHostedOrganizationDetails : Organization
MaxAutoscaleSeats = MaxAutoscaleSeats, MaxAutoscaleSeats = MaxAutoscaleSeats,
OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling, OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling,
LimitCollectionCreationDeletion = LimitCollectionCreationDeletion, LimitCollectionCreationDeletion = LimitCollectionCreationDeletion,
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
FlexibleCollections = FlexibleCollections
}; };
} }
} }

View File

@ -0,0 +1,12 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
namespace Bit.Core.AdminConsole.Providers.Interfaces;
public interface IRemoveOrganizationFromProviderCommand
{
Task RemoveOrganizationFromProvider(
Provider provider,
ProviderOrganization providerOrganization,
Organization organization);
}

View File

@ -23,7 +23,6 @@ public interface IProviderService
Task AddOrganizationsToReseller(Guid providerId, IEnumerable<Guid> organizationIds); Task AddOrganizationsToReseller(Guid providerId, IEnumerable<Guid> organizationIds);
Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId, OrganizationSignup organizationSignup, Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId, OrganizationSignup organizationSignup,
string clientOwnerEmail, User user); string clientOwnerEmail, User user);
Task RemoveOrganizationAsync(Guid providerId, Guid providerOrganizationId, Guid removingUserId);
Task LogProviderAccessToOrganizationAsync(Guid organizationId); Task LogProviderAccessToOrganizationAsync(Guid organizationId);
Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid ownerId); Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid ownerId);
Task SendProviderSetupInviteEmailAsync(Provider provider, string ownerEmail); Task SendProviderSetupInviteEmailAsync(Provider provider, string ownerEmail);

View File

@ -65,8 +65,6 @@ public class OrganizationService : IOrganizationService
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory; private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
public OrganizationService( public OrganizationService(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
@ -418,6 +416,9 @@ public class OrganizationService : IOrganizationService
} }
} }
/// <summary>
/// Create a new organization in a cloud environment
/// </summary>
public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup signup, public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup signup,
bool provider = false) bool provider = false)
{ {
@ -440,8 +441,9 @@ public class OrganizationService : IOrganizationService
await ValidateSignUpPoliciesAsync(signup.Owner.Id); await ValidateSignUpPoliciesAsync(signup.Owner.Id);
} }
var flexibleCollectionsIsEnabled = var flexibleCollectionsSignupEnabled =
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsSignup, _currentContext);
var flexibleCollectionsV1IsEnabled = var flexibleCollectionsV1IsEnabled =
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1, _currentContext); _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1, _currentContext);
@ -482,7 +484,15 @@ public class OrganizationService : IOrganizationService
Status = OrganizationStatusType.Created, Status = OrganizationStatusType.Created,
UsePasswordManager = true, UsePasswordManager = true,
UseSecretsManager = signup.UseSecretsManager, UseSecretsManager = signup.UseSecretsManager,
LimitCollectionCreationDeletion = !flexibleCollectionsIsEnabled,
// This feature flag indicates that new organizations should be automatically onboarded to
// Flexible Collections enhancements
FlexibleCollections = flexibleCollectionsSignupEnabled,
// 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.
// 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
}; };
@ -534,6 +544,9 @@ public class OrganizationService : IOrganizationService
} }
} }
/// <summary>
/// Create a new organization on a self-hosted instance
/// </summary>
public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync( public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(
OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey, OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey,
string privateKey) string privateKey)
@ -558,10 +571,8 @@ public class OrganizationService : IOrganizationService
await ValidateSignUpPoliciesAsync(owner.Id); await ValidateSignUpPoliciesAsync(owner.Id);
var flexibleCollectionsMvpIsEnabled = var flexibleCollectionsSignupEnabled =
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsSignup, _currentContext);
var flexibleCollectionsV1IsEnabled =
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1, _currentContext);
var organization = new Organization var organization = new Organization
{ {
@ -603,8 +614,12 @@ public class OrganizationService : IOrganizationService
UseSecretsManager = license.UseSecretsManager, UseSecretsManager = license.UseSecretsManager,
SmSeats = license.SmSeats, SmSeats = license.SmSeats,
SmServiceAccounts = license.SmServiceAccounts, SmServiceAccounts = license.SmServiceAccounts,
LimitCollectionCreationDeletion = !flexibleCollectionsMvpIsEnabled || license.LimitCollectionCreationDeletion, LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion,
AllowAdminAccessToAllCollectionItems = !flexibleCollectionsV1IsEnabled || license.AllowAdminAccessToAllCollectionItems AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems,
// This feature flag indicates that new organizations should be automatically onboarded to
// Flexible Collections enhancements
FlexibleCollections = flexibleCollectionsSignupEnabled,
}; };
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false); var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
@ -616,6 +631,10 @@ public class OrganizationService : IOrganizationService
return result; return result;
} }
/// <summary>
/// Private helper method to create a new organization.
/// This is common code used by both the cloud and self-hosted methods.
/// </summary>
private async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(Organization organization, private async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(Organization organization,
Guid ownerId, string ownerKey, string collectionName, bool withPayment) Guid ownerId, string ownerKey, string collectionName, bool withPayment)
{ {
@ -829,6 +848,7 @@ public class OrganizationService : IOrganizationService
{ {
var inviteTypes = new HashSet<OrganizationUserType>(invites.Where(i => i.invite.Type.HasValue) var inviteTypes = new HashSet<OrganizationUserType>(invites.Where(i => i.invite.Type.HasValue)
.Select(i => i.invite.Type.Value)); .Select(i => i.invite.Type.Value));
if (invitingUserId.HasValue && inviteTypes.Count > 0) if (invitingUserId.HasValue && inviteTypes.Count > 0)
{ {
foreach (var (invite, _) in invites) foreach (var (invite, _) in invites)
@ -2008,7 +2028,11 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException("Custom users can only grant the same custom permissions that they have."); throw new BadRequestException("Custom users can only grant the same custom permissions that they have.");
} }
if (FlexibleCollectionsIsEnabled && newType == OrganizationUserType.Manager && oldType is not OrganizationUserType.Manager) // TODO: pass in the whole organization object when this is refactored into a command/query
// See AC-2036
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
var flexibleCollectionsEnabled = organizationAbility?.FlexibleCollections ?? false;
if (flexibleCollectionsEnabled && newType == OrganizationUserType.Manager && oldType is not OrganizationUserType.Manager)
{ {
throw new BadRequestException("Manager role is deprecated after Flexible Collections."); throw new BadRequestException("Manager role is deprecated after Flexible Collections.");
} }

View File

@ -0,0 +1,8 @@
using Bit.Core.AdminConsole.Entities;
namespace Bit.Core.Billing.Commands;
public interface IRemovePaymentMethodCommand
{
Task RemovePaymentMethod(Organization organization);
}

View File

@ -0,0 +1,140 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Braintree;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Billing.Commands.Implementations;
public class RemovePaymentMethodCommand : IRemovePaymentMethodCommand
{
private readonly IBraintreeGateway _braintreeGateway;
private readonly ILogger<RemovePaymentMethodCommand> _logger;
private readonly IStripeAdapter _stripeAdapter;
public RemovePaymentMethodCommand(
IBraintreeGateway braintreeGateway,
ILogger<RemovePaymentMethodCommand> logger,
IStripeAdapter stripeAdapter)
{
_braintreeGateway = braintreeGateway;
_logger = logger;
_stripeAdapter = stripeAdapter;
}
public async Task RemovePaymentMethod(Organization organization)
{
const string braintreeCustomerIdKey = "btCustomerId";
if (organization == null)
{
throw new ArgumentNullException(nameof(organization));
}
if (organization.Gateway is not GatewayType.Stripe || string.IsNullOrEmpty(organization.GatewayCustomerId))
{
throw ContactSupport();
}
var stripeCustomer = await _stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId, new Stripe.CustomerGetOptions
{
Expand = new List<string> { "invoice_settings.default_payment_method", "sources" }
});
if (stripeCustomer == null)
{
_logger.LogError("Could not find Stripe customer ({ID}) when removing payment method", organization.GatewayCustomerId);
throw ContactSupport();
}
if (stripeCustomer.Metadata?.TryGetValue(braintreeCustomerIdKey, out var braintreeCustomerId) ?? false)
{
await RemoveBraintreePaymentMethodAsync(braintreeCustomerId);
}
else
{
await RemoveStripePaymentMethodsAsync(stripeCustomer);
}
}
private async Task RemoveBraintreePaymentMethodAsync(string braintreeCustomerId)
{
var customer = await _braintreeGateway.Customer.FindAsync(braintreeCustomerId);
if (customer == null)
{
_logger.LogError("Failed to retrieve Braintree customer ({ID}) when removing payment method", braintreeCustomerId);
throw ContactSupport();
}
if (customer.DefaultPaymentMethod != null)
{
var existingDefaultPaymentMethod = customer.DefaultPaymentMethod;
var updateCustomerResult = await _braintreeGateway.Customer.UpdateAsync(
braintreeCustomerId,
new CustomerRequest { DefaultPaymentMethodToken = null });
if (!updateCustomerResult.IsSuccess())
{
_logger.LogError("Failed to update payment method for Braintree customer ({ID}) | Message: {Message}",
braintreeCustomerId, updateCustomerResult.Message);
throw ContactSupport();
}
var deletePaymentMethodResult = await _braintreeGateway.PaymentMethod.DeleteAsync(existingDefaultPaymentMethod.Token);
if (!deletePaymentMethodResult.IsSuccess())
{
await _braintreeGateway.Customer.UpdateAsync(
braintreeCustomerId,
new CustomerRequest { DefaultPaymentMethodToken = existingDefaultPaymentMethod.Token });
_logger.LogError(
"Failed to delete Braintree payment method for Customer ({ID}), re-linked payment method. Message: {Message}",
braintreeCustomerId, deletePaymentMethodResult.Message);
throw ContactSupport();
}
}
else
{
_logger.LogWarning("Tried to remove non-existent Braintree payment method for Customer ({ID})", braintreeCustomerId);
}
}
private async Task RemoveStripePaymentMethodsAsync(Stripe.Customer customer)
{
if (customer.Sources != null && customer.Sources.Any())
{
foreach (var source in customer.Sources)
{
switch (source)
{
case Stripe.BankAccount:
await _stripeAdapter.BankAccountDeleteAsync(customer.Id, source.Id);
break;
case Stripe.Card:
await _stripeAdapter.CardDeleteAsync(customer.Id, source.Id);
break;
}
}
}
var paymentMethods = _stripeAdapter.PaymentMethodListAutoPagingAsync(new Stripe.PaymentMethodListOptions
{
Customer = customer.Id
});
await foreach (var paymentMethod in paymentMethods)
{
await _stripeAdapter.PaymentMethodDetachAsync(paymentMethod.Id, new Stripe.PaymentMethodDetachOptions());
}
}
private static GatewayException ContactSupport() => new("Could not remove your payment method. Please contact support for assistance.");
}

View File

@ -0,0 +1,14 @@
using Bit.Core.Billing.Commands;
using Bit.Core.Billing.Commands.Implementations;
namespace Bit.Core.Billing.Extensions;
using Microsoft.Extensions.DependencyInjection;
public static class ServiceCollectionExtensions
{
public static void AddBillingCommands(this IServiceCollection services)
{
services.AddSingleton<IRemovePaymentMethodCommand, RemovePaymentMethodCommand>();
}
}

View File

@ -96,12 +96,17 @@ public static class FeatureFlagKeys
public const string VaultOnboarding = "vault-onboarding"; public const string VaultOnboarding = "vault-onboarding";
public const string AutofillV2 = "autofill-v2"; public const string AutofillV2 = "autofill-v2";
public const string BrowserFilelessImport = "browser-fileless-import"; public const string BrowserFilelessImport = "browser-fileless-import";
public const string FlexibleCollections = "flexible-collections"; /// <summary>
/// Deprecated - never used, do not use. Will always default to false. Will be deleted as part of Flexible Collections cleanup
/// </summary>
public const string FlexibleCollections = "flexible-collections-disabled-do-not-use";
public const string FlexibleCollectionsV1 = "flexible-collections-v-1"; // v-1 is intentional public const string FlexibleCollectionsV1 = "flexible-collections-v-1"; // v-1 is intentional
public const string BulkCollectionAccess = "bulk-collection-access"; public const string BulkCollectionAccess = "bulk-collection-access";
public const string AutofillOverlay = "autofill-overlay"; public const string AutofillOverlay = "autofill-overlay";
public const string ItemShare = "item-share"; public const string ItemShare = "item-share";
public const string KeyRotationImprovements = "key-rotation-improvements"; public const string KeyRotationImprovements = "key-rotation-improvements";
public const string FlexibleCollectionsMigration = "flexible-collections-migration";
public const string FlexibleCollectionsSignup = "flexible-collections-signup";
public static List<string> GetAllKeys() public static List<string> GetAllKeys()
{ {

View File

@ -5,7 +5,6 @@ using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Identity; using Bit.Core.Identity;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -26,8 +25,6 @@ public class CurrentContext : ICurrentContext
private IEnumerable<ProviderOrganizationProviderDetails> _providerOrganizationProviderDetails; private IEnumerable<ProviderOrganizationProviderDetails> _providerOrganizationProviderDetails;
private IEnumerable<ProviderUserOrganizationDetails> _providerUserOrganizations; private IEnumerable<ProviderUserOrganizationDetails> _providerUserOrganizations;
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, this);
public virtual HttpContext HttpContext { get; set; } public virtual HttpContext HttpContext { get; set; }
public virtual Guid? UserId { get; set; } public virtual Guid? UserId { get; set; }
public virtual User User { get; set; } public virtual User User { get; set; }
@ -283,11 +280,6 @@ public class CurrentContext : ICurrentContext
public async Task<bool> OrganizationManager(Guid orgId) public async Task<bool> OrganizationManager(Guid orgId)
{ {
if (FlexibleCollectionsIsEnabled)
{
throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF.");
}
return await OrganizationAdmin(orgId) || return await OrganizationAdmin(orgId) ||
(Organizations?.Any(o => o.Id == orgId && o.Type == OrganizationUserType.Manager) ?? false); (Organizations?.Any(o => o.Id == orgId && o.Type == OrganizationUserType.Manager) ?? false);
} }
@ -350,22 +342,12 @@ public class CurrentContext : ICurrentContext
public async Task<bool> EditAssignedCollections(Guid orgId) public async Task<bool> EditAssignedCollections(Guid orgId)
{ {
if (FlexibleCollectionsIsEnabled)
{
throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF.");
}
return await OrganizationManager(orgId) || (Organizations?.Any(o => o.Id == orgId return await OrganizationManager(orgId) || (Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.EditAssignedCollections ?? false)) ?? false); && (o.Permissions?.EditAssignedCollections ?? false)) ?? false);
} }
public async Task<bool> DeleteAssignedCollections(Guid orgId) public async Task<bool> DeleteAssignedCollections(Guid orgId)
{ {
if (FlexibleCollectionsIsEnabled)
{
throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF.");
}
return await OrganizationManager(orgId) || (Organizations?.Any(o => o.Id == orgId return await OrganizationManager(orgId) || (Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.DeleteAssignedCollections ?? false)) ?? false); && (o.Permissions?.DeleteAssignedCollections ?? false)) ?? false);
} }
@ -378,11 +360,6 @@ public class CurrentContext : ICurrentContext
* This entire method will be moved to the CollectionAuthorizationHandler in the future * This entire method will be moved to the CollectionAuthorizationHandler in the future
*/ */
if (FlexibleCollectionsIsEnabled)
{
throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF.");
}
var org = GetOrganization(orgId); var org = GetOrganization(orgId);
return await EditAssignedCollections(orgId) return await EditAssignedCollections(orgId)
|| await DeleteAssignedCollections(orgId) || await DeleteAssignedCollections(orgId)

View File

@ -52,7 +52,7 @@
<PackageReference Include="Serilog.Sinks.AzureCosmosDB" Version="2.0.0" /> <PackageReference Include="Serilog.Sinks.AzureCosmosDB" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="2.0.9" /> <PackageReference Include="Serilog.Sinks.SyslogMessages" Version="2.0.9" />
<PackageReference Include="AspNetCoreRateLimit" Version="4.0.2" /> <PackageReference Include="AspNetCoreRateLimit" Version="4.0.2" />
<PackageReference Include="Braintree" Version="5.21.0" /> <PackageReference Include="Braintree" Version="5.23.0" />
<PackageReference Include="Stripe.net" Version="40.16.0" /> <PackageReference Include="Stripe.net" Version="40.16.0" />
<PackageReference Include="Otp.NET" Version="1.2.2" /> <PackageReference Include="Otp.NET" Version="1.2.2" />
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" /> <PackageReference Include="YubicoDotNetClient" Version="1.2.0" />

View File

@ -0,0 +1,27 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" 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;">
<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 20px; -webkit-text-size-adjust: none;" valign="top">
Your organization, {{OrganizationName}}, is no longer managed by {{ProviderName}}. Please update your billing information.
</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 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 0 20px; -webkit-text-size-adjust: none;" valign="top">
To maintain your subscription, update your organization billing information by navigating to the web vault -> Organization -> Billing -> <a target="_blank" clicktracking=off href="{{PaymentMethodUrl}}" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;">Payment Method</a>.
</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 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 0 20px; -webkit-text-size-adjust: none;" valign="top">
For more information, please refer to the following help article: <a target="_blank" clicktracking=off href="https://bitwarden.com/help/update-billing-info/#update-billing-information-for-organizations" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;">Update billing information for organizations</a>
</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="{{{PaymentMethodUrl}}}" 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;">
Add payment method
</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}}

View File

@ -0,0 +1,7 @@
{{#>BasicTextLayout}}
Your organization, {{OrganizationName}}, is no longer managed by {{ProviderName}}. Please update your billing information.
To maintain your subscription, update your organization billing information by navigating to the web vault -> Organization -> Billing -> Payment Method.
Or click the following link: {{{link PaymentMethodUrl}}}
{{/BasicTextLayout}}

View File

@ -0,0 +1,11 @@
namespace Bit.Core.Models.Mail.Provider;
public class ProviderUpdatePaymentMethodViewModel : BaseMailModel
{
public string OrganizationId { get; set; }
public string OrganizationName { get; set; }
public string ProviderName { get; set; }
public string PaymentMethodUrl =>
$"{WebVaultUrl}/organizations/{OrganizationId}/billing/payment-method";
}

View File

@ -2,7 +2,6 @@
using System.Text.Json; using System.Text.Json;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations;
@ -18,21 +17,15 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman
private readonly ILicensingService _licensingService; private readonly ILicensingService _licensingService;
private readonly IGlobalSettings _globalSettings; private readonly IGlobalSettings _globalSettings;
private readonly IOrganizationService _organizationService; private readonly IOrganizationService _organizationService;
private readonly IFeatureService _featureService;
private readonly ICurrentContext _currentContext;
public UpdateOrganizationLicenseCommand( public UpdateOrganizationLicenseCommand(
ILicensingService licensingService, ILicensingService licensingService,
IGlobalSettings globalSettings, IGlobalSettings globalSettings,
IOrganizationService organizationService, IOrganizationService organizationService)
IFeatureService featureService,
ICurrentContext currentContext)
{ {
_licensingService = licensingService; _licensingService = licensingService;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_organizationService = organizationService; _organizationService = organizationService;
_featureService = featureService;
_currentContext = currentContext;
} }
public async Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrganization, public async Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrganization,
@ -65,10 +58,8 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman
private async Task UpdateOrganizationAsync(SelfHostedOrganizationDetails selfHostedOrganizationDetails, OrganizationLicense license) private async Task UpdateOrganizationAsync(SelfHostedOrganizationDetails selfHostedOrganizationDetails, OrganizationLicense license)
{ {
var flexibleCollectionsMvpIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
var flexibleCollectionsV1IsEnabled = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1, _currentContext);
var organization = selfHostedOrganizationDetails.ToOrganization(); var organization = selfHostedOrganizationDetails.ToOrganization();
organization.UpdateFromLicense(license, flexibleCollectionsMvpIsEnabled, flexibleCollectionsV1IsEnabled); organization.UpdateFromLicense(license);
await _organizationService.ReplaceAndUpdateCacheAsync(organization); await _organizationService.ReplaceAndUpdateCacheAsync(organization);
} }

View File

@ -8,6 +8,9 @@ namespace Bit.Core.Services;
public interface IApplicationCacheService public interface IApplicationCacheService
{ {
Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync(); Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync();
#nullable enable
Task<OrganizationAbility?> GetOrganizationAbilityAsync(Guid orgId);
#nullable disable
Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync(); Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync();
Task UpsertOrganizationAbilityAsync(Organization organization); Task UpsertOrganizationAbilityAsync(Organization organization);
Task UpsertProviderAbilityAsync(Provider provider); Task UpsertProviderAbilityAsync(Provider provider);

View File

@ -60,6 +60,11 @@ public interface IMailService
Task SendProviderInviteEmailAsync(string providerName, ProviderUser providerUser, string token, string email); Task SendProviderInviteEmailAsync(string providerName, ProviderUser providerUser, string token, string email);
Task SendProviderConfirmedEmailAsync(string providerName, string email); Task SendProviderConfirmedEmailAsync(string providerName, string email);
Task SendProviderUserRemoved(string providerName, string email); Task SendProviderUserRemoved(string providerName, string email);
Task SendProviderUpdatePaymentMethod(
Guid organizationId,
string organizationName,
string providerName,
IEnumerable<string> emails);
Task SendUpdatedTempPasswordEmailAsync(string email, string userName); Task SendUpdatedTempPasswordEmailAsync(string email, string userName);
Task SendFamiliesForEnterpriseOfferEmailAsync(string sponsorOrgName, string email, bool existingAccount, string token); Task SendFamiliesForEnterpriseOfferEmailAsync(string sponsorOrgName, string email, bool existingAccount, string token);
Task BulkSendFamiliesForEnterpriseOfferEmailAsync(string SponsorOrgName, IEnumerable<(string Email, bool ExistingAccount, string Token)> invites); Task BulkSendFamiliesForEnterpriseOfferEmailAsync(string SponsorOrgName, IEnumerable<(string Email, bool ExistingAccount, string Token)> invites);

View File

@ -49,4 +49,5 @@ public interface IPaymentService
Task ArchiveTaxRateAsync(TaxRate taxRate); Task ArchiveTaxRateAsync(TaxRate taxRate);
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);
} }

View File

@ -23,6 +23,7 @@ public interface IStripeAdapter
Task<Stripe.Invoice> InvoiceDeleteAsync(string id, Stripe.InvoiceDeleteOptions options = null); Task<Stripe.Invoice> InvoiceDeleteAsync(string id, Stripe.InvoiceDeleteOptions options = null);
Task<Stripe.Invoice> InvoiceVoidInvoiceAsync(string id, Stripe.InvoiceVoidOptions options = null); Task<Stripe.Invoice> InvoiceVoidInvoiceAsync(string id, Stripe.InvoiceVoidOptions options = null);
IEnumerable<Stripe.PaymentMethod> PaymentMethodListAutoPaging(Stripe.PaymentMethodListOptions options); IEnumerable<Stripe.PaymentMethod> PaymentMethodListAutoPaging(Stripe.PaymentMethodListOptions options);
IAsyncEnumerable<Stripe.PaymentMethod> PaymentMethodListAutoPagingAsync(Stripe.PaymentMethodListOptions options);
Task<Stripe.PaymentMethod> PaymentMethodAttachAsync(string id, Stripe.PaymentMethodAttachOptions options = null); Task<Stripe.PaymentMethod> PaymentMethodAttachAsync(string id, Stripe.PaymentMethodAttachOptions options = null);
Task<Stripe.PaymentMethod> PaymentMethodDetachAsync(string id, Stripe.PaymentMethodDetachOptions options = null); Task<Stripe.PaymentMethod> PaymentMethodDetachAsync(string id, Stripe.PaymentMethodDetachOptions options = null);
Task<Stripe.TaxRate> TaxRateCreateAsync(Stripe.TaxRateCreateOptions options); Task<Stripe.TaxRate> TaxRateCreateAsync(Stripe.TaxRateCreateOptions options);

View File

@ -754,6 +754,30 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message); await _mailDeliveryService.SendEmailAsync(message);
} }
public async Task SendProviderUpdatePaymentMethod(
Guid organizationId,
string organizationName,
string providerName,
IEnumerable<string> emails)
{
var message = CreateDefaultMessage("Update your billing information", emails);
var model = new ProviderUpdatePaymentMethodViewModel
{
OrganizationId = organizationId.ToString(),
OrganizationName = CoreHelpers.SanitizeForEmail(organizationName),
ProviderName = CoreHelpers.SanitizeForEmail(providerName),
SiteName = _globalSettings.SiteName,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash
};
await AddMessageContentAsync(message, "Provider.ProviderUpdatePaymentMethod", model);
message.Category = "ProviderUpdatePaymentMethod";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendUpdatedTempPasswordEmailAsync(string email, string userName) public async Task SendUpdatedTempPasswordEmailAsync(string email, string userName)
{ {
var message = CreateDefaultMessage("Master Password Has Been Changed", email); var message = CreateDefaultMessage("Master Password Has Been Changed", email);

View File

@ -30,6 +30,15 @@ public class InMemoryApplicationCacheService : IApplicationCacheService
return _orgAbilities; return _orgAbilities;
} }
#nullable enable
public async Task<OrganizationAbility?> GetOrganizationAbilityAsync(Guid organizationId)
{
(await GetOrganizationAbilitiesAsync())
.TryGetValue(organizationId, out var organizationAbility);
return organizationAbility;
}
#nullable disable
public virtual async Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync() public virtual async Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync()
{ {
await InitProviderAbilitiesAsync(); await InitProviderAbilitiesAsync();

View File

@ -138,6 +138,9 @@ public class StripeAdapter : IStripeAdapter
return _paymentMethodService.ListAutoPaging(options); return _paymentMethodService.ListAutoPaging(options);
} }
public IAsyncEnumerable<Stripe.PaymentMethod> PaymentMethodListAutoPagingAsync(Stripe.PaymentMethodListOptions options)
=> _paymentMethodService.ListAutoPagingAsync(options);
public Task<Stripe.PaymentMethod> PaymentMethodAttachAsync(string id, Stripe.PaymentMethodAttachOptions options = null) public Task<Stripe.PaymentMethod> PaymentMethodAttachAsync(string id, Stripe.PaymentMethodAttachOptions options = null)
{ {
return _paymentMethodService.AttachAsync(id, options); return _paymentMethodService.AttachAsync(id, options);

View File

@ -1614,6 +1614,23 @@ public class StripePaymentService : IPaymentService
return await FinalizeSubscriptionChangeAsync(org, new SecretsManagerSubscribeUpdate(org, plan, additionalSmSeats, additionalServiceAccount), prorationDate); return await FinalizeSubscriptionChangeAsync(org, new SecretsManagerSubscribeUpdate(org, plan, additionalSmSeats, additionalServiceAccount), prorationDate);
} }
public async Task<bool> RisksSubscriptionFailure(Organization organization)
{
var subscriptionInfo = await GetSubscriptionAsync(organization);
if (subscriptionInfo.Subscription is not { Status: "active" or "trialing" or "past_due" } ||
subscriptionInfo.UpcomingInvoice == null)
{
return false;
}
var customer = await GetCustomerAsync(organization.GatewayCustomerId);
var paymentSource = await GetBillingPaymentSourceAsync(customer);
return paymentSource == null;
}
private Stripe.PaymentMethod GetLatestCardPaymentMethod(string customerId) private Stripe.PaymentMethod GetLatestCardPaymentMethod(string customerId)
{ {
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging( var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(

View File

@ -197,6 +197,9 @@ public class NoopMailService : IMailService
return Task.FromResult(0); return Task.FromResult(0);
} }
public Task SendProviderUpdatePaymentMethod(Guid organizationId, string organizationName, string providerName,
IEnumerable<string> emails) => Task.FromResult(0);
public Task SendUpdatedTempPasswordEmailAsync(string email, string userName) public Task SendUpdatedTempPasswordEmailAsync(string email, string userName)
{ {
return Task.FromResult(0); return Task.FromResult(0);

View File

@ -788,7 +788,7 @@ public class CipherService : ICipherService
{ {
collection.SetNewId(); collection.SetNewId();
newCollections.Add(collection); newCollections.Add(collection);
if (UseFlexibleCollections) if (org.FlexibleCollections)
{ {
newCollectionUsers.Add(new CollectionUser newCollectionUsers.Add(new CollectionUser
{ {

View File

@ -35,8 +35,6 @@ public class CollectController : Controller
_featureService = featureService; _featureService = featureService;
} }
bool UseFlexibleCollections => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
[HttpPost] [HttpPost]
public async Task<IActionResult> Post([FromBody] IEnumerable<EventModel> model) public async Task<IActionResult> Post([FromBody] IEnumerable<EventModel> model)
{ {

View File

@ -7,14 +7,21 @@ using Bit.Core.Repositories;
using Bit.Core.Settings; using Bit.Core.Settings;
using Dapper; using Dapper;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
namespace Bit.Infrastructure.Dapper.Repositories; namespace Bit.Infrastructure.Dapper.Repositories;
public class OrganizationRepository : Repository<Organization, Guid>, IOrganizationRepository public class OrganizationRepository : Repository<Organization, Guid>, IOrganizationRepository
{ {
public OrganizationRepository(GlobalSettings globalSettings) private readonly ILogger<OrganizationRepository> _logger;
public OrganizationRepository(
GlobalSettings globalSettings,
ILogger<OrganizationRepository> logger)
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
{ } {
_logger = logger;
}
public OrganizationRepository(string connectionString, string readOnlyConnectionString) public OrganizationRepository(string connectionString, string readOnlyConnectionString)
: base(connectionString, readOnlyConnectionString) : base(connectionString, readOnlyConnectionString)
@ -153,6 +160,8 @@ public class OrganizationRepository : Repository<Organization, Guid>, IOrganizat
public async Task<IEnumerable<string>> GetOwnerEmailAddressesById(Guid organizationId) public async Task<IEnumerable<string>> GetOwnerEmailAddressesById(Guid organizationId)
{ {
_logger.LogInformation("AC-1758: Executing GetOwnerEmailAddressesById (Dapper)");
await using var connection = new SqlConnection(ConnectionString); await using var connection = new SqlConnection(ConnectionString);
return await connection.QueryAsync<string>( return await connection.QueryAsync<string>(

View File

@ -5,7 +5,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper" Version="2.1.24" /> <PackageReference Include="Dapper" Version="2.1.28" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -5,15 +5,23 @@ using Bit.Core.Models.Data.Organizations;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Organization = Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization; using Organization = Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization;
namespace Bit.Infrastructure.EntityFramework.Repositories; namespace Bit.Infrastructure.EntityFramework.Repositories;
public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Organization, Organization, Guid>, IOrganizationRepository public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Organization, Organization, Guid>, IOrganizationRepository
{ {
public OrganizationRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) private readonly ILogger<OrganizationRepository> _logger;
: base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Organizations)
{ } public OrganizationRepository(
IServiceScopeFactory serviceScopeFactory,
IMapper mapper,
ILogger<OrganizationRepository> logger)
: base(serviceScopeFactory, mapper, context => context.Organizations)
{
_logger = logger;
}
public async Task<Core.AdminConsole.Entities.Organization> GetByIdentifierAsync(string identifier) public async Task<Core.AdminConsole.Entities.Organization> GetByIdentifierAsync(string identifier)
{ {
@ -240,6 +248,8 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
public async Task<IEnumerable<string>> GetOwnerEmailAddressesById(Guid organizationId) public async Task<IEnumerable<string>> GetOwnerEmailAddressesById(Guid organizationId)
{ {
_logger.LogInformation("AC-1758: Executing GetOwnerEmailAddressesById (Entity Framework)");
using var scope = ServiceScopeFactory.CreateScope(); using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope); var dbContext = GetDatabaseContext(scope);

View File

@ -10,6 +10,7 @@ public class GrantEntityTypeConfiguration : IEntityTypeConfiguration<Grant>
{ {
builder builder
.HasKey(s => s.Id) .HasKey(s => s.Id)
.HasName("PK_Grant")
.IsClustered(); .IsClustered();
builder builder

View File

@ -3,9 +3,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="linq2db" Version="5.3.2" /> <PackageReference Include="linq2db" Version="5.3.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.15" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.15" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.15" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.11" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.11" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" />
<PackageReference Include="linq2db.EntityFrameworkCore" Version="7.6.0" /> <PackageReference Include="linq2db.EntityFrameworkCore" Version="7.6.0" />

View File

@ -37,7 +37,6 @@ public class OrganizationsControllerTests : IDisposable
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IPaymentService _paymentService; private readonly IPaymentService _paymentService;
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly IProviderRepository _providerRepository;
private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly ISsoConfigService _ssoConfigService; private readonly ISsoConfigService _ssoConfigService;
private readonly IUserService _userService; private readonly IUserService _userService;
@ -46,7 +45,6 @@ public class OrganizationsControllerTests : IDisposable
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery; private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand; private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
private readonly IUpdateOrganizationLicenseCommand _updateOrganizationLicenseCommand;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly ILicensingService _licensingService; private readonly ILicensingService _licensingService;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
@ -64,7 +62,6 @@ public class OrganizationsControllerTests : IDisposable
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>(); _organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
_paymentService = Substitute.For<IPaymentService>(); _paymentService = Substitute.For<IPaymentService>();
_policyRepository = Substitute.For<IPolicyRepository>(); _policyRepository = Substitute.For<IPolicyRepository>();
_providerRepository = Substitute.For<IProviderRepository>();
_ssoConfigRepository = Substitute.For<ISsoConfigRepository>(); _ssoConfigRepository = Substitute.For<ISsoConfigRepository>();
_ssoConfigService = Substitute.For<ISsoConfigService>(); _ssoConfigService = Substitute.For<ISsoConfigService>();
_getOrganizationApiKeyQuery = Substitute.For<IGetOrganizationApiKeyQuery>(); _getOrganizationApiKeyQuery = Substitute.For<IGetOrganizationApiKeyQuery>();
@ -73,19 +70,33 @@ public class OrganizationsControllerTests : IDisposable
_userService = Substitute.For<IUserService>(); _userService = Substitute.For<IUserService>();
_cloudGetOrganizationLicenseQuery = Substitute.For<ICloudGetOrganizationLicenseQuery>(); _cloudGetOrganizationLicenseQuery = Substitute.For<ICloudGetOrganizationLicenseQuery>();
_createOrganizationApiKeyCommand = Substitute.For<ICreateOrganizationApiKeyCommand>(); _createOrganizationApiKeyCommand = Substitute.For<ICreateOrganizationApiKeyCommand>();
_updateOrganizationLicenseCommand = Substitute.For<IUpdateOrganizationLicenseCommand>();
_featureService = Substitute.For<IFeatureService>(); _featureService = Substitute.For<IFeatureService>();
_licensingService = Substitute.For<ILicensingService>(); _licensingService = Substitute.For<ILicensingService>();
_updateSecretsManagerSubscriptionCommand = Substitute.For<IUpdateSecretsManagerSubscriptionCommand>(); _updateSecretsManagerSubscriptionCommand = Substitute.For<IUpdateSecretsManagerSubscriptionCommand>();
_upgradeOrganizationPlanCommand = Substitute.For<IUpgradeOrganizationPlanCommand>(); _upgradeOrganizationPlanCommand = Substitute.For<IUpgradeOrganizationPlanCommand>();
_addSecretsManagerSubscriptionCommand = Substitute.For<IAddSecretsManagerSubscriptionCommand>(); _addSecretsManagerSubscriptionCommand = Substitute.For<IAddSecretsManagerSubscriptionCommand>();
_sut = new OrganizationsController(_organizationRepository, _organizationUserRepository, _sut = new OrganizationsController(
_policyRepository, _providerRepository, _organizationService, _userService, _paymentService, _currentContext, _organizationRepository,
_ssoConfigRepository, _ssoConfigService, _getOrganizationApiKeyQuery, _rotateOrganizationApiKeyCommand, _organizationUserRepository,
_createOrganizationApiKeyCommand, _organizationApiKeyRepository, _updateOrganizationLicenseCommand, _policyRepository,
_cloudGetOrganizationLicenseQuery, _featureService, _globalSettings, _licensingService, _organizationService,
_updateSecretsManagerSubscriptionCommand, _upgradeOrganizationPlanCommand, _addSecretsManagerSubscriptionCommand); _userService,
_paymentService,
_currentContext,
_ssoConfigRepository,
_ssoConfigService,
_getOrganizationApiKeyQuery,
_rotateOrganizationApiKeyCommand,
_createOrganizationApiKeyCommand,
_organizationApiKeyRepository,
_cloudGetOrganizationLicenseQuery,
_featureService,
_globalSettings,
_licensingService,
_updateSecretsManagerSubscriptionCommand,
_upgradeOrganizationPlanCommand,
_addSecretsManagerSubscriptionCommand);
} }
public void Dispose() public void Dispose()

View File

@ -2,16 +2,14 @@
using Bit.Api.Controllers; using Bit.Api.Controllers;
using Bit.Api.Models.Request; using Bit.Api.Models.Request;
using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Test.AutoFixture;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -22,16 +20,17 @@ namespace Bit.Api.Test.Controllers;
[ControllerCustomize(typeof(CollectionsController))] [ControllerCustomize(typeof(CollectionsController))]
[SutProviderCustomize] [SutProviderCustomize]
[FeatureServiceCustomize(FeatureFlagKeys.FlexibleCollections)]
public class CollectionsControllerTests public class CollectionsControllerTests
{ {
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task Post_Success(Guid orgId, CollectionRequestModel collectionRequest, public async Task Post_Success(OrganizationAbility organizationAbility, CollectionRequestModel collectionRequest,
SutProvider<CollectionsController> sutProvider) SutProvider<CollectionsController> sutProvider)
{ {
ArrangeOrganizationAbility(sutProvider, organizationAbility);
Collection ExpectedCollection() => Arg.Is<Collection>(c => Collection ExpectedCollection() => Arg.Is<Collection>(c =>
c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId && c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId &&
c.OrganizationId == orgId); c.OrganizationId == organizationAbility.Id);
sutProvider.GetDependency<IAuthorizationService>() sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
@ -39,7 +38,7 @@ public class CollectionsControllerTests
Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Create))) Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Create)))
.Returns(AuthorizationResult.Success()); .Returns(AuthorizationResult.Success());
_ = await sutProvider.Sut.Post(orgId, collectionRequest); _ = await sutProvider.Sut.Post(organizationAbility.Id, collectionRequest);
await sutProvider.GetDependency<ICollectionService>() await sutProvider.GetDependency<ICollectionService>()
.Received(1) .Received(1)
@ -49,8 +48,11 @@ public class CollectionsControllerTests
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task Put_Success(Collection collection, CollectionRequestModel collectionRequest, public async Task Put_Success(Collection collection, CollectionRequestModel collectionRequest,
SutProvider<CollectionsController> sutProvider) SutProvider<CollectionsController> sutProvider, OrganizationAbility organizationAbility)
{ {
ArrangeOrganizationAbility(sutProvider, organizationAbility);
collection.OrganizationId = organizationAbility.Id;
Collection ExpectedCollection() => Arg.Is<Collection>(c => c.Id == collection.Id && Collection ExpectedCollection() => Arg.Is<Collection>(c => c.Id == collection.Id &&
c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId && c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId &&
c.OrganizationId == collection.OrganizationId); c.OrganizationId == collection.OrganizationId);
@ -75,8 +77,11 @@ public class CollectionsControllerTests
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task Put_WithNoCollectionPermission_ThrowsNotFound(Collection collection, CollectionRequestModel collectionRequest, public async Task Put_WithNoCollectionPermission_ThrowsNotFound(Collection collection, CollectionRequestModel collectionRequest,
SutProvider<CollectionsController> sutProvider) SutProvider<CollectionsController> sutProvider, OrganizationAbility organizationAbility)
{ {
ArrangeOrganizationAbility(sutProvider, organizationAbility);
collection.OrganizationId = organizationAbility.Id;
sutProvider.GetDependency<IAuthorizationService>() sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
collection, collection,
@ -91,8 +96,11 @@ public class CollectionsControllerTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetOrganizationCollectionsWithGroups_WithReadAllPermissions_GetsAllCollections(Organization organization, Guid userId, SutProvider<CollectionsController> sutProvider) public async Task GetOrganizationCollectionsWithGroups_WithReadAllPermissions_GetsAllCollections(OrganizationAbility organizationAbility,
Guid userId, SutProvider<CollectionsController> sutProvider)
{ {
ArrangeOrganizationAbility(sutProvider, organizationAbility);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<IAuthorizationService>() sutProvider.GetDependency<IAuthorizationService>()
@ -102,18 +110,20 @@ public class CollectionsControllerTests
Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements => Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>
requirements.Cast<CollectionOperationRequirement>().All(operation => requirements.Cast<CollectionOperationRequirement>().All(operation =>
operation.Name == nameof(CollectionOperations.ReadAllWithAccess) operation.Name == nameof(CollectionOperations.ReadAllWithAccess)
&& operation.OrganizationId == organization.Id))) && operation.OrganizationId == organizationAbility.Id)))
.Returns(AuthorizationResult.Success()); .Returns(AuthorizationResult.Success());
await sutProvider.Sut.GetManyWithDetails(organization.Id); await sutProvider.Sut.GetManyWithDetails(organizationAbility.Id);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdWithAccessAsync(userId, organization.Id, Arg.Any<bool>()); await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdWithAccessAsync(userId, organizationAbility.Id, Arg.Any<bool>());
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdWithAccessAsync(organization.Id); await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdWithAccessAsync(organizationAbility.Id);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetOrganizationCollectionsWithGroups_MissingReadAllPermissions_GetsAssignedCollections(Organization organization, Guid userId, SutProvider<CollectionsController> sutProvider) public async Task GetOrganizationCollectionsWithGroups_MissingReadAllPermissions_GetsAssignedCollections(
OrganizationAbility organizationAbility, Guid userId, SutProvider<CollectionsController> sutProvider)
{ {
ArrangeOrganizationAbility(sutProvider, organizationAbility);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<IAuthorizationService>() sutProvider.GetDependency<IAuthorizationService>()
@ -123,7 +133,7 @@ public class CollectionsControllerTests
Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements => Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>
requirements.Cast<CollectionOperationRequirement>().All(operation => requirements.Cast<CollectionOperationRequirement>().All(operation =>
operation.Name == nameof(CollectionOperations.ReadAllWithAccess) operation.Name == nameof(CollectionOperations.ReadAllWithAccess)
&& operation.OrganizationId == organization.Id))) && operation.OrganizationId == organizationAbility.Id)))
.Returns(AuthorizationResult.Failed()); .Returns(AuthorizationResult.Failed());
sutProvider.GetDependency<IAuthorizationService>() sutProvider.GetDependency<IAuthorizationService>()
@ -135,15 +145,19 @@ public class CollectionsControllerTests
operation.Name == nameof(BulkCollectionOperations.ReadWithAccess)))) operation.Name == nameof(BulkCollectionOperations.ReadWithAccess))))
.Returns(AuthorizationResult.Success()); .Returns(AuthorizationResult.Success());
await sutProvider.Sut.GetManyWithDetails(organization.Id); await sutProvider.Sut.GetManyWithDetails(organizationAbility.Id);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdWithAccessAsync(userId, organization.Id, Arg.Any<bool>()); await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdWithAccessAsync(userId, organizationAbility.Id, Arg.Any<bool>());
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceive().GetManyByOrganizationIdWithAccessAsync(organization.Id); await sutProvider.GetDependency<ICollectionRepository>().DidNotReceive().GetManyByOrganizationIdWithAccessAsync(organizationAbility.Id);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetOrganizationCollections_WithReadAllPermissions_GetsAllCollections(Organization organization, ICollection<Collection> collections, Guid userId, SutProvider<CollectionsController> sutProvider) public async Task GetOrganizationCollections_WithReadAllPermissions_GetsAllCollections(
OrganizationAbility organizationAbility, List<Collection> collections, Guid userId, SutProvider<CollectionsController> sutProvider)
{ {
ArrangeOrganizationAbility(sutProvider, organizationAbility);
collections.ForEach(c => c.OrganizationId = organizationAbility.Id);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<IAuthorizationService>() sutProvider.GetDependency<IAuthorizationService>()
@ -153,26 +167,30 @@ public class CollectionsControllerTests
Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements => Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>
requirements.Cast<CollectionOperationRequirement>().All(operation => requirements.Cast<CollectionOperationRequirement>().All(operation =>
operation.Name == nameof(CollectionOperations.ReadAll) operation.Name == nameof(CollectionOperations.ReadAll)
&& operation.OrganizationId == organization.Id))) && operation.OrganizationId == organizationAbility.Id)))
.Returns(AuthorizationResult.Success()); .Returns(AuthorizationResult.Success());
sutProvider.GetDependency<ICollectionRepository>() sutProvider.GetDependency<ICollectionRepository>()
.GetManyByOrganizationIdAsync(organization.Id) .GetManyByOrganizationIdAsync(organizationAbility.Id)
.Returns(collections); .Returns(collections);
var response = await sutProvider.Sut.Get(organization.Id); var response = await sutProvider.Sut.Get(organizationAbility.Id);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdAsync(organization.Id); await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdAsync(organizationAbility.Id);
Assert.Equal(collections.Count, response.Data.Count()); Assert.Equal(collections.Count, response.Data.Count());
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetOrganizationCollections_MissingReadAllPermissions_GetsManageableCollections(Organization organization, ICollection<CollectionDetails> collections, Guid userId, SutProvider<CollectionsController> sutProvider) public async Task GetOrganizationCollections_MissingReadAllPermissions_GetsManageableCollections(
OrganizationAbility organizationAbility, List<CollectionDetails> collections, Guid userId, SutProvider<CollectionsController> sutProvider)
{ {
collections.First().OrganizationId = organization.Id; ArrangeOrganizationAbility(sutProvider, organizationAbility);
collections.First().Manage = true; collections.ForEach(c => c.OrganizationId = organizationAbility.Id);
collections.Skip(1).First().OrganizationId = organization.Id; collections.ForEach(c => c.Manage = false);
var managedCollection = collections.First();
managedCollection.Manage = true;
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
@ -183,7 +201,7 @@ public class CollectionsControllerTests
Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements => Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>
requirements.Cast<CollectionOperationRequirement>().All(operation => requirements.Cast<CollectionOperationRequirement>().All(operation =>
operation.Name == nameof(CollectionOperations.ReadAll) operation.Name == nameof(CollectionOperations.ReadAll)
&& operation.OrganizationId == organization.Id))) && operation.OrganizationId == organizationAbility.Id)))
.Returns(AuthorizationResult.Failed()); .Returns(AuthorizationResult.Failed());
sutProvider.GetDependency<IAuthorizationService>() sutProvider.GetDependency<IAuthorizationService>()
@ -196,22 +214,27 @@ public class CollectionsControllerTests
.Returns(AuthorizationResult.Success()); .Returns(AuthorizationResult.Success());
sutProvider.GetDependency<ICollectionRepository>() sutProvider.GetDependency<ICollectionRepository>()
.GetManyByUserIdAsync(userId, true) .GetManyByUserIdAsync(userId, false)
.Returns(collections); .Returns(collections);
var result = await sutProvider.Sut.Get(organization.Id); var result = await sutProvider.Sut.Get(organizationAbility.Id);
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceive().GetManyByOrganizationIdAsync(organization.Id); await sutProvider.GetDependency<ICollectionRepository>().DidNotReceive().GetManyByOrganizationIdAsync(organizationAbility.Id);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdAsync(userId, true); await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdAsync(userId, false);
Assert.Single(result.Data); Assert.Single(result.Data);
Assert.All(result.Data, c => Assert.Equal(organization.Id, c.OrganizationId)); Assert.All(result.Data, c => Assert.Equal(organizationAbility.Id, c.OrganizationId));
Assert.All(result.Data, c => Assert.Equal(managedCollection.Id, c.Id));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task DeleteMany_Success(Guid orgId, Collection collection1, Collection collection2, SutProvider<CollectionsController> sutProvider) public async Task DeleteMany_Success(OrganizationAbility organizationAbility, Collection collection1, Collection collection2,
SutProvider<CollectionsController> sutProvider)
{ {
// Arrange // Arrange
var orgId = organizationAbility.Id;
ArrangeOrganizationAbility(sutProvider, organizationAbility);
var model = new CollectionBulkDeleteRequestModel var model = new CollectionBulkDeleteRequestModel
{ {
Ids = new[] { collection1.Id, collection2.Id } Ids = new[] { collection1.Id, collection2.Id }
@ -251,9 +274,13 @@ public class CollectionsControllerTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task DeleteMany_PermissionDenied_ThrowsNotFound(Guid orgId, Collection collection1, Collection collection2, SutProvider<CollectionsController> sutProvider) public async Task DeleteMany_PermissionDenied_ThrowsNotFound(OrganizationAbility organizationAbility, Collection collection1,
Collection collection2, SutProvider<CollectionsController> sutProvider)
{ {
// Arrange // Arrange
var orgId = organizationAbility.Id;
ArrangeOrganizationAbility(sutProvider, organizationAbility);
var model = new CollectionBulkDeleteRequestModel var model = new CollectionBulkDeleteRequestModel
{ {
Ids = new[] { collection1.Id, collection2.Id } Ids = new[] { collection1.Id, collection2.Id }
@ -292,9 +319,13 @@ public class CollectionsControllerTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task PostBulkCollectionAccess_Success(User actingUser, ICollection<Collection> collections, SutProvider<CollectionsController> sutProvider) public async Task PostBulkCollectionAccess_Success(User actingUser, List<Collection> collections,
OrganizationAbility organizationAbility, SutProvider<CollectionsController> sutProvider)
{ {
// Arrange // Arrange
ArrangeOrganizationAbility(sutProvider, organizationAbility);
collections.ForEach(c => c.OrganizationId = organizationAbility.Id);
var userId = Guid.NewGuid(); var userId = Guid.NewGuid();
var groupId = Guid.NewGuid(); var groupId = Guid.NewGuid();
var model = new BulkCollectionAccessRequestModel var model = new BulkCollectionAccessRequestModel
@ -321,7 +352,7 @@ public class CollectionsControllerTests
IEnumerable<Collection> ExpectedCollectionAccess() => Arg.Is<IEnumerable<Collection>>(cols => cols.SequenceEqual(collections)); IEnumerable<Collection> ExpectedCollectionAccess() => Arg.Is<IEnumerable<Collection>>(cols => cols.SequenceEqual(collections));
// Act // Act
await sutProvider.Sut.PostBulkCollectionAccess(model); await sutProvider.Sut.PostBulkCollectionAccess(organizationAbility.Id, model);
// Assert // Assert
await sutProvider.GetDependency<IAuthorizationService>().Received().AuthorizeAsync( await sutProvider.GetDependency<IAuthorizationService>().Received().AuthorizeAsync(
@ -338,8 +369,13 @@ public class CollectionsControllerTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task PostBulkCollectionAccess_CollectionsNotFound_Throws(User actingUser, ICollection<Collection> collections, SutProvider<CollectionsController> sutProvider) public async Task PostBulkCollectionAccess_CollectionsNotFound_Throws(User actingUser,
OrganizationAbility organizationAbility, List<Collection> collections,
SutProvider<CollectionsController> sutProvider)
{ {
ArrangeOrganizationAbility(sutProvider, organizationAbility);
collections.ForEach(c => c.OrganizationId = organizationAbility.Id);
var userId = Guid.NewGuid(); var userId = Guid.NewGuid();
var groupId = Guid.NewGuid(); var groupId = Guid.NewGuid();
var model = new BulkCollectionAccessRequestModel var model = new BulkCollectionAccessRequestModel
@ -356,7 +392,8 @@ public class CollectionsControllerTests
.GetManyByManyIdsAsync(model.CollectionIds) .GetManyByManyIdsAsync(model.CollectionIds)
.Returns(collections.Skip(1).ToList()); .Returns(collections.Skip(1).ToList());
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PostBulkCollectionAccess(model)); var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.PostBulkCollectionAccess(organizationAbility.Id, model));
Assert.Equal("One or more collections not found.", exception.Message); Assert.Equal("One or more collections not found.", exception.Message);
await sutProvider.GetDependency<IAuthorizationService>().DidNotReceiveWithAnyArgs().AuthorizeAsync( await sutProvider.GetDependency<IAuthorizationService>().DidNotReceiveWithAnyArgs().AuthorizeAsync(
@ -369,8 +406,81 @@ public class CollectionsControllerTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task PostBulkCollectionAccess_AccessDenied_Throws(User actingUser, ICollection<Collection> collections, SutProvider<CollectionsController> sutProvider) public async Task PostBulkCollectionAccess_CollectionsBelongToDifferentOrganizations_Throws(User actingUser,
OrganizationAbility organizationAbility, List<Collection> collections,
SutProvider<CollectionsController> sutProvider)
{ {
ArrangeOrganizationAbility(sutProvider, organizationAbility);
// First collection has a different orgId
collections.Skip(1).ToList().ForEach(c => c.OrganizationId = organizationAbility.Id);
var userId = Guid.NewGuid();
var groupId = Guid.NewGuid();
var model = new BulkCollectionAccessRequestModel
{
CollectionIds = collections.Select(c => c.Id),
Users = new[] { new SelectionReadOnlyRequestModel { Id = userId, Manage = true } },
Groups = new[] { new SelectionReadOnlyRequestModel { Id = groupId, ReadOnly = true } },
};
sutProvider.GetDependency<ICurrentContext>()
.UserId.Returns(actingUser.Id);
sutProvider.GetDependency<ICollectionRepository>()
.GetManyByManyIdsAsync(model.CollectionIds)
.Returns(collections);
var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.PostBulkCollectionAccess(organizationAbility.Id, model));
Assert.Equal("One or more collections not found.", exception.Message);
await sutProvider.GetDependency<IAuthorizationService>().DidNotReceiveWithAnyArgs().AuthorizeAsync(
Arg.Any<ClaimsPrincipal>(),
Arg.Any<IEnumerable<Collection>>(),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()
);
await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().DidNotReceiveWithAnyArgs()
.AddAccessAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task PostBulkCollectionAccess_FlexibleCollectionsDisabled_Throws(OrganizationAbility organizationAbility, List<Collection> collections,
SutProvider<CollectionsController> sutProvider)
{
organizationAbility.FlexibleCollections = false;
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
.Returns(organizationAbility);
var userId = Guid.NewGuid();
var groupId = Guid.NewGuid();
var model = new BulkCollectionAccessRequestModel
{
CollectionIds = collections.Select(c => c.Id),
Users = new[] { new SelectionReadOnlyRequestModel { Id = userId, Manage = true } },
Groups = new[] { new SelectionReadOnlyRequestModel { Id = groupId, ReadOnly = true } },
};
var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.PostBulkCollectionAccess(organizationAbility.Id, model));
Assert.Equal("Feature disabled.", exception.Message);
await sutProvider.GetDependency<IAuthorizationService>().DidNotReceiveWithAnyArgs().AuthorizeAsync(
Arg.Any<ClaimsPrincipal>(),
Arg.Any<IEnumerable<Collection>>(),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()
);
await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().DidNotReceiveWithAnyArgs()
.AddAccessAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task PostBulkCollectionAccess_AccessDenied_Throws(User actingUser, List<Collection> collections,
OrganizationAbility organizationAbility, SutProvider<CollectionsController> sutProvider)
{
ArrangeOrganizationAbility(sutProvider, organizationAbility);
collections.ForEach(c => c.OrganizationId = organizationAbility.Id);
var userId = Guid.NewGuid(); var userId = Guid.NewGuid();
var groupId = Guid.NewGuid(); var groupId = Guid.NewGuid();
var model = new BulkCollectionAccessRequestModel var model = new BulkCollectionAccessRequestModel
@ -396,7 +506,7 @@ public class CollectionsControllerTests
IEnumerable<Collection> ExpectedCollectionAccess() => Arg.Is<IEnumerable<Collection>>(cols => cols.SequenceEqual(collections)); IEnumerable<Collection> ExpectedCollectionAccess() => Arg.Is<IEnumerable<Collection>>(cols => cols.SequenceEqual(collections));
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PostBulkCollectionAccess(model)); await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PostBulkCollectionAccess(organizationAbility.Id, model));
await sutProvider.GetDependency<IAuthorizationService>().Received().AuthorizeAsync( await sutProvider.GetDependency<IAuthorizationService>().Received().AuthorizeAsync(
Arg.Any<ClaimsPrincipal>(), Arg.Any<ClaimsPrincipal>(),
ExpectedCollectionAccess(), ExpectedCollectionAccess(),
@ -406,4 +516,12 @@ public class CollectionsControllerTests
await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().DidNotReceiveWithAnyArgs() await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().DidNotReceiveWithAnyArgs()
.AddAccessAsync(default, default, default); .AddAccessAsync(default, default, default);
} }
private void ArrangeOrganizationAbility(SutProvider<CollectionsController> sutProvider, OrganizationAbility organizationAbility)
{
organizationAbility.FlexibleCollections = true;
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
.Returns(organizationAbility);
}
} }

View File

@ -1,6 +1,5 @@
using System.Security.Claims; using System.Security.Claims;
using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -9,7 +8,6 @@ using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Test.AutoFixture;
using Bit.Core.Test.Vault.AutoFixture; using Bit.Core.Test.Vault.AutoFixture;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
@ -20,7 +18,6 @@ using Xunit;
namespace Bit.Api.Test.Vault.AuthorizationHandlers; namespace Bit.Api.Test.Vault.AuthorizationHandlers;
[SutProviderCustomize] [SutProviderCustomize]
[FeatureServiceCustomize(FeatureFlagKeys.FlexibleCollections)]
public class BulkCollectionAuthorizationHandlerTests public class BulkCollectionAuthorizationHandlerTests
{ {
[Theory, CollectionCustomization] [Theory, CollectionCustomization]
@ -35,7 +32,7 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = userType; organization.Type = userType;
organization.Permissions = new Permissions(); organization.Permissions = new Permissions();
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true); ArrangeOrganizationAbility(sutProvider, organization, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Create }, new[] { BulkCollectionOperations.Create },
@ -44,7 +41,6 @@ public class BulkCollectionAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
@ -61,7 +57,7 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = OrganizationUserType.User; organization.Type = OrganizationUserType.User;
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, false); ArrangeOrganizationAbility(sutProvider, organization, false);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Create }, new[] { BulkCollectionOperations.Create },
@ -70,7 +66,6 @@ public class BulkCollectionAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
@ -97,7 +92,7 @@ public class BulkCollectionAuthorizationHandlerTests
ManageUsers = false ManageUsers = false
}; };
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true); ArrangeOrganizationAbility(sutProvider, organization, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Create }, new[] { BulkCollectionOperations.Create },
@ -106,7 +101,6 @@ public class BulkCollectionAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false); sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
@ -117,10 +111,12 @@ public class BulkCollectionAuthorizationHandlerTests
[Theory, BitAutoData, CollectionCustomization] [Theory, BitAutoData, CollectionCustomization]
public async Task CanCreateAsync_WhenMissingOrgAccess_NoSuccess( public async Task CanCreateAsync_WhenMissingOrgAccess_NoSuccess(
Guid userId, Guid userId,
ICollection<Collection> collections, CurrentContextOrganization organization,
List<Collection> collections,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider) SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
{ {
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(collections.First().OrganizationId, true); collections.ForEach(c => c.OrganizationId = organization.Id);
ArrangeOrganizationAbility(sutProvider, organization, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Create }, new[] { BulkCollectionOperations.Create },
@ -130,7 +126,6 @@ public class BulkCollectionAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null); sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false); sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
@ -747,7 +742,7 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = userType; organization.Type = userType;
organization.Permissions = new Permissions(); organization.Permissions = new Permissions();
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true); ArrangeOrganizationAbility(sutProvider, organization, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete }, new[] { BulkCollectionOperations.Delete },
@ -756,8 +751,6 @@ public class BulkCollectionAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()
.Returns(organizationAbilities);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
@ -778,7 +771,7 @@ public class BulkCollectionAuthorizationHandlerTests
DeleteAnyCollection = true DeleteAnyCollection = true
}; };
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true); ArrangeOrganizationAbility(sutProvider, organization, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete }, new[] { BulkCollectionOperations.Delete },
@ -787,8 +780,6 @@ public class BulkCollectionAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()
.Returns(organizationAbilities);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
@ -806,13 +797,11 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = OrganizationUserType.User; organization.Type = OrganizationUserType.User;
organization.Permissions = new Permissions(); organization.Permissions = new Permissions();
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, false); ArrangeOrganizationAbility(sutProvider, organization, false);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId, Arg.Any<bool>()).Returns(collections); sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId, Arg.Any<bool>()).Returns(collections);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()
.Returns(organizationAbilities);
foreach (var c in collections) foreach (var c in collections)
{ {
@ -849,7 +838,7 @@ public class BulkCollectionAuthorizationHandlerTests
ManageUsers = false ManageUsers = false
}; };
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true); ArrangeOrganizationAbility(sutProvider, organization, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete }, new[] { BulkCollectionOperations.Delete },
@ -858,8 +847,6 @@ public class BulkCollectionAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()
.Returns(organizationAbilities);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false); sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
@ -981,17 +968,16 @@ public class BulkCollectionAuthorizationHandlerTests
} }
} }
private static Dictionary<Guid, OrganizationAbility> ArrangeOrganizationAbilitiesDictionary(Guid orgId, private static void ArrangeOrganizationAbility(
bool limitCollectionCreationDeletion) SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
CurrentContextOrganization organization, bool limitCollectionCreationDeletion)
{ {
return new Dictionary<Guid, OrganizationAbility> var organizationAbility = new OrganizationAbility();
{ organizationAbility.Id = organization.Id;
{ orgId, organizationAbility.FlexibleCollections = true;
new OrganizationAbility organizationAbility.LimitCollectionCreationDeletion = limitCollectionCreationDeletion;
{
LimitCollectionCreationDeletion = limitCollectionCreationDeletion sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
} .Returns(organizationAbility);
}
};
} }
} }

View File

@ -1,10 +1,8 @@
using System.Security.Claims; using System.Security.Claims;
using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Test.AutoFixture;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -14,7 +12,6 @@ using Xunit;
namespace Bit.Api.Test.Vault.AuthorizationHandlers; namespace Bit.Api.Test.Vault.AuthorizationHandlers;
[SutProviderCustomize] [SutProviderCustomize]
[FeatureServiceCustomize(FeatureFlagKeys.FlexibleCollections)]
public class CollectionAuthorizationHandlerTests public class CollectionAuthorizationHandlerTests
{ {
[Theory] [Theory]

View File

@ -1,12 +1,10 @@
using System.Security.Claims; using System.Security.Claims;
using Bit.Api.Vault.AuthorizationHandlers.Groups; using Bit.Api.Vault.AuthorizationHandlers.Groups;
using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Test.AutoFixture;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -16,7 +14,6 @@ using Xunit;
namespace Bit.Api.Test.Vault.AuthorizationHandlers; namespace Bit.Api.Test.Vault.AuthorizationHandlers;
[SutProviderCustomize] [SutProviderCustomize]
[FeatureServiceCustomize(FeatureFlagKeys.FlexibleCollections)]
public class GroupAuthorizationHandlerTests public class GroupAuthorizationHandlerTests
{ {
[Theory] [Theory]
@ -30,7 +27,7 @@ public class GroupAuthorizationHandlerTests
organization.Type = userType; organization.Type = userType;
organization.Permissions = new Permissions(); organization.Permissions = new Permissions();
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true); ArrangeOrganizationAbility(sutProvider, organization, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { GroupOperations.ReadAll(organization.Id) }, new[] { GroupOperations.ReadAll(organization.Id) },
@ -39,7 +36,6 @@ public class GroupAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
@ -54,7 +50,7 @@ public class GroupAuthorizationHandlerTests
organization.Type = OrganizationUserType.User; organization.Type = OrganizationUserType.User;
organization.Permissions = new Permissions(); organization.Permissions = new Permissions();
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true); ArrangeOrganizationAbility(sutProvider, organization, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { GroupOperations.ReadAll(organization.Id) }, new[] { GroupOperations.ReadAll(organization.Id) },
@ -64,7 +60,6 @@ public class GroupAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.UserId .UserId
.Returns(userId); .Returns(userId);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.ProviderUserForOrgAsync(organization.Id) .ProviderUserForOrgAsync(organization.Id)
.Returns(true); .Returns(true);
@ -97,7 +92,7 @@ public class GroupAuthorizationHandlerTests
ManageUsers = manageUsers ManageUsers = manageUsers
}; };
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, limitCollectionCreationDeletion); ArrangeOrganizationAbility(sutProvider, organization, limitCollectionCreationDeletion);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { GroupOperations.ReadAll(organization.Id) }, new[] { GroupOperations.ReadAll(organization.Id) },
@ -106,7 +101,6 @@ public class GroupAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
@ -133,7 +127,7 @@ public class GroupAuthorizationHandlerTests
AccessImportExport = false AccessImportExport = false
}; };
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true); ArrangeOrganizationAbility(sutProvider, organization, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { GroupOperations.ReadAll(organization.Id) }, new[] { GroupOperations.ReadAll(organization.Id) },
@ -142,7 +136,6 @@ public class GroupAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false); sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
@ -153,20 +146,19 @@ public class GroupAuthorizationHandlerTests
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task CanReadAllAsync_WhenMissingOrgAccess_NoSuccess( public async Task CanReadAllAsync_WhenMissingOrgAccess_NoSuccess(
Guid userId, Guid userId,
Guid organizationId, CurrentContextOrganization organization,
SutProvider<GroupAuthorizationHandler> sutProvider) SutProvider<GroupAuthorizationHandler> sutProvider)
{ {
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organizationId, true); ArrangeOrganizationAbility(sutProvider, organization, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { GroupOperations.ReadAll(organizationId) }, new[] { GroupOperations.ReadAll(organization.Id) },
new ClaimsPrincipal(), new ClaimsPrincipal(),
null null
); );
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null); sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false); sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
@ -210,17 +202,16 @@ public class GroupAuthorizationHandlerTests
Assert.True(context.HasFailed); Assert.True(context.HasFailed);
} }
private static Dictionary<Guid, OrganizationAbility> ArrangeOrganizationAbilitiesDictionary(Guid orgId, private static void ArrangeOrganizationAbility(
bool limitCollectionCreationDeletion) SutProvider<GroupAuthorizationHandler> sutProvider,
CurrentContextOrganization organization, bool limitCollectionCreationDeletion)
{ {
return new Dictionary<Guid, OrganizationAbility> var organizationAbility = new OrganizationAbility();
{ organizationAbility.Id = organization.Id;
{ orgId, organizationAbility.FlexibleCollections = true;
new OrganizationAbility organizationAbility.LimitCollectionCreationDeletion = limitCollectionCreationDeletion;
{
LimitCollectionCreationDeletion = limitCollectionCreationDeletion sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
} .Returns(organizationAbility);
}
};
} }
} }

View File

@ -1,12 +1,10 @@
using System.Security.Claims; using System.Security.Claims;
using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers; using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers;
using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Test.AutoFixture;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -16,7 +14,6 @@ using Xunit;
namespace Bit.Api.Test.Vault.AuthorizationHandlers; namespace Bit.Api.Test.Vault.AuthorizationHandlers;
[SutProviderCustomize] [SutProviderCustomize]
[FeatureServiceCustomize(FeatureFlagKeys.FlexibleCollections)]
public class OrganizationUserAuthorizationHandlerTests public class OrganizationUserAuthorizationHandlerTests
{ {
[Theory] [Theory]
@ -30,7 +27,7 @@ public class OrganizationUserAuthorizationHandlerTests
organization.Type = userType; organization.Type = userType;
organization.Permissions = new Permissions(); organization.Permissions = new Permissions();
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true); ArrangeOrganizationAbility(sutProvider, organization, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { OrganizationUserOperations.ReadAll(organization.Id) }, new[] { OrganizationUserOperations.ReadAll(organization.Id) },
@ -39,7 +36,6 @@ public class OrganizationUserAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
@ -54,7 +50,7 @@ public class OrganizationUserAuthorizationHandlerTests
organization.Type = OrganizationUserType.User; organization.Type = OrganizationUserType.User;
organization.Permissions = new Permissions(); organization.Permissions = new Permissions();
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true); ArrangeOrganizationAbility(sutProvider, organization, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { OrganizationUserOperations.ReadAll(organization.Id) }, new[] { OrganizationUserOperations.ReadAll(organization.Id) },
@ -64,7 +60,6 @@ public class OrganizationUserAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.UserId .UserId
.Returns(userId); .Returns(userId);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.ProviderUserForOrgAsync(organization.Id) .ProviderUserForOrgAsync(organization.Id)
.Returns(true); .Returns(true);
@ -97,7 +92,7 @@ public class OrganizationUserAuthorizationHandlerTests
ManageUsers = manageUsers ManageUsers = manageUsers
}; };
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, limitCollectionCreationDeletion); ArrangeOrganizationAbility(sutProvider, organization, limitCollectionCreationDeletion);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { OrganizationUserOperations.ReadAll(organization.Id) }, new[] { OrganizationUserOperations.ReadAll(organization.Id) },
@ -106,7 +101,6 @@ public class OrganizationUserAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
@ -132,7 +126,7 @@ public class OrganizationUserAuthorizationHandlerTests
ManageUsers = false ManageUsers = false
}; };
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true); ArrangeOrganizationAbility(sutProvider, organization, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { OrganizationUserOperations.ReadAll(organization.Id) }, new[] { OrganizationUserOperations.ReadAll(organization.Id) },
@ -141,7 +135,6 @@ public class OrganizationUserAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false); sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
@ -152,20 +145,19 @@ public class OrganizationUserAuthorizationHandlerTests
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task HandleRequirementAsync_WhenMissingOrgAccess_NoSuccess( public async Task HandleRequirementAsync_WhenMissingOrgAccess_NoSuccess(
Guid userId, Guid userId,
Guid organizationId, CurrentContextOrganization organization,
SutProvider<OrganizationUserAuthorizationHandler> sutProvider) SutProvider<OrganizationUserAuthorizationHandler> sutProvider)
{ {
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organizationId, true); ArrangeOrganizationAbility(sutProvider, organization, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { OrganizationUserOperations.ReadAll(organizationId) }, new[] { OrganizationUserOperations.ReadAll(organization.Id) },
new ClaimsPrincipal(), new ClaimsPrincipal(),
null null
); );
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null); sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false); sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
@ -207,17 +199,16 @@ public class OrganizationUserAuthorizationHandlerTests
Assert.True(context.HasFailed); Assert.True(context.HasFailed);
} }
private static Dictionary<Guid, OrganizationAbility> ArrangeOrganizationAbilitiesDictionary(Guid orgId, private static void ArrangeOrganizationAbility(
bool limitCollectionCreationDeletion) SutProvider<OrganizationUserAuthorizationHandler> sutProvider,
CurrentContextOrganization organization, bool limitCollectionCreationDeletion)
{ {
return new Dictionary<Guid, OrganizationAbility> var organizationAbility = new OrganizationAbility();
{ organizationAbility.Id = organization.Id;
{ orgId, organizationAbility.FlexibleCollections = true;
new OrganizationAbility organizationAbility.LimitCollectionCreationDeletion = limitCollectionCreationDeletion;
{
LimitCollectionCreationDeletion = limitCollectionCreationDeletion sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
} .Returns(organizationAbility);
}
};
} }
} }

View File

@ -15,6 +15,7 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Models.Mail; using Bit.Core.Models.Mail;
using Bit.Core.Models.StaticStore; using Bit.Core.Models.StaticStore;
@ -972,21 +973,23 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task InviteUser_WithFCEnabled_WhenInvitingManager_Throws(Organization organization, OrganizationUserInvite invite, public async Task InviteUser_WithFCEnabled_WhenInvitingManager_Throws(OrganizationAbility organizationAbility,
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider) OrganizationUserInvite invite, OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
{ {
invite.Type = OrganizationUserType.Manager; invite.Type = OrganizationUserType.Manager;
organizationAbility.FlexibleCollections = true;
sutProvider.GetDependency<IFeatureService>() sutProvider.GetDependency<IApplicationCacheService>()
.IsEnabled(FeatureFlagKeys.FlexibleCollections, Arg.Any<ICurrentContext>()) .GetOrganizationAbilityAsync(organizationAbility.Id)
.Returns(true); .Returns(organizationAbility);
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.ManageUsers(organization.Id) .ManageUsers(organizationAbility.Id)
.Returns(true); .Returns(true);
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, new (OrganizationUserInvite, string)[] { (invite, null) })); () => sutProvider.Sut.InviteUsersAsync(organizationAbility.Id, invitor.UserId,
new (OrganizationUserInvite, string)[] { (invite, null) }));
Assert.Contains("manager role is deprecated", exception.Message.ToLowerInvariant()); Assert.Contains("manager role is deprecated", exception.Message.ToLowerInvariant());
} }
@ -1273,19 +1276,20 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task SaveUser_WithFCEnabled_WhenUpgradingToManager_Throws( public async Task SaveUser_WithFCEnabled_WhenUpgradingToManager_Throws(
Organization organization, OrganizationAbility organizationAbility,
[OrganizationUser(type: OrganizationUserType.User)] OrganizationUser oldUserData, [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser oldUserData,
[OrganizationUser(type: OrganizationUserType.Manager)] OrganizationUser newUserData, [OrganizationUser(type: OrganizationUserType.Manager)] OrganizationUser newUserData,
IEnumerable<CollectionAccessSelection> collections, IEnumerable<CollectionAccessSelection> collections,
IEnumerable<Guid> groups, IEnumerable<Guid> groups,
SutProvider<OrganizationService> sutProvider) SutProvider<OrganizationService> sutProvider)
{ {
sutProvider.GetDependency<IFeatureService>() organizationAbility.FlexibleCollections = true;
.IsEnabled(FeatureFlagKeys.FlexibleCollections, Arg.Any<ICurrentContext>()) sutProvider.GetDependency<IApplicationCacheService>()
.Returns(true); .GetOrganizationAbilityAsync(organizationAbility.Id)
.Returns(organizationAbility);
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.ManageUsers(organization.Id) .ManageUsers(organizationAbility.Id)
.Returns(true); .Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>() sutProvider.GetDependency<IOrganizationUserRepository>()
@ -1294,7 +1298,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
newUserData.Id = oldUserData.Id; newUserData.Id = oldUserData.Id;
newUserData.UserId = oldUserData.UserId; newUserData.UserId = oldUserData.UserId;
newUserData.OrganizationId = oldUserData.OrganizationId = organization.Id; newUserData.OrganizationId = oldUserData.OrganizationId = organizationAbility.Id;
newUserData.Permissions = CoreHelpers.ClassToJsonData(new Permissions()); newUserData.Permissions = CoreHelpers.ClassToJsonData(new Permissions());
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(

View File

@ -1,75 +0,0 @@
using System.Reflection;
using AutoFixture;
using AutoFixture.Kernel;
using Bit.Core.Context;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
namespace Bit.Core.Test.AutoFixture;
internal class FeatureServiceBuilder : ISpecimenBuilder
{
private readonly string _enabledFeatureFlag;
public FeatureServiceBuilder(string enabledFeatureFlag)
{
_enabledFeatureFlag = enabledFeatureFlag;
}
public object Create(object request, ISpecimenContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (request is not ParameterInfo pi)
{
return new NoSpecimen();
}
if (pi.ParameterType == typeof(IFeatureService))
{
var fixture = new Fixture();
var featureService = fixture.WithAutoNSubstitutions().Create<IFeatureService>();
featureService
.IsEnabled(_enabledFeatureFlag, Arg.Any<ICurrentContext>(), Arg.Any<bool>())
.Returns(true);
return featureService;
}
return new NoSpecimen();
}
}
internal class FeatureServiceCustomization : ICustomization
{
private readonly string _enabledFeatureFlag;
public FeatureServiceCustomization(string enabledFeatureFlag)
{
_enabledFeatureFlag = enabledFeatureFlag;
}
public void Customize(IFixture fixture)
{
fixture.Customizations.Add(new FeatureServiceBuilder(_enabledFeatureFlag));
}
}
/// <summary>
/// Arranges the IFeatureService mock to enable the specified feature flag
/// </summary>
public class FeatureServiceCustomizeAttribute : BitCustomizeAttribute
{
private readonly string _enabledFeatureFlag;
public FeatureServiceCustomizeAttribute(string enabledFeatureFlag)
{
_enabledFeatureFlag = enabledFeatureFlag;
}
public override ICustomization GetCustomization() => new FeatureServiceCustomization(_enabledFeatureFlag);
}

View File

@ -0,0 +1,367 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Commands.Implementations;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
using BT = Braintree;
using S = Stripe;
namespace Bit.Core.Test.Billing.Commands;
[SutProviderCustomize]
public class RemovePaymentMethodCommandTests
{
[Theory, BitAutoData]
public async Task RemovePaymentMethod_NullOrganization_ArgumentNullException(
SutProvider<RemovePaymentMethodCommand> sutProvider) =>
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.RemovePaymentMethod(null));
[Theory, BitAutoData]
public async Task RemovePaymentMethod_NonStripeGateway_ContactSupport(
Organization organization,
SutProvider<RemovePaymentMethodCommand> sutProvider)
{
organization.Gateway = GatewayType.BitPay;
await ThrowsContactSupportAsync(() => sutProvider.Sut.RemovePaymentMethod(organization));
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_NoGatewayCustomerId_ContactSupport(
Organization organization,
SutProvider<RemovePaymentMethodCommand> sutProvider)
{
organization.Gateway = GatewayType.Stripe;
organization.GatewayCustomerId = null;
await ThrowsContactSupportAsync(() => sutProvider.Sut.RemovePaymentMethod(organization));
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_NoStripeCustomer_ContactSupport(
Organization organization,
SutProvider<RemovePaymentMethodCommand> sutProvider)
{
organization.Gateway = GatewayType.Stripe;
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<S.CustomerGetOptions>())
.ReturnsNull();
await ThrowsContactSupportAsync(() => sutProvider.Sut.RemovePaymentMethod(organization));
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Braintree_NoCustomer_ContactSupport(
Organization organization,
SutProvider<RemovePaymentMethodCommand> sutProvider)
{
organization.Gateway = GatewayType.Stripe;
const string braintreeCustomerId = "1";
var stripeCustomer = new S.Customer
{
Metadata = new Dictionary<string, string>
{
{ "btCustomerId", braintreeCustomerId }
}
};
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<S.CustomerGetOptions>())
.Returns(stripeCustomer);
var (braintreeGateway, customerGateway, paymentMethodGateway) = Setup(sutProvider.GetDependency<BT.IBraintreeGateway>());
customerGateway.FindAsync(braintreeCustomerId).ReturnsNull();
braintreeGateway.Customer.Returns(customerGateway);
await ThrowsContactSupportAsync(() => sutProvider.Sut.RemovePaymentMethod(organization));
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
await customerGateway.DidNotReceiveWithAnyArgs()
.UpdateAsync(Arg.Any<string>(), Arg.Any<BT.CustomerRequest>());
await paymentMethodGateway.DidNotReceiveWithAnyArgs().DeleteAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Braintree_NoPaymentMethod_NoOp(
Organization organization,
SutProvider<RemovePaymentMethodCommand> sutProvider)
{
organization.Gateway = GatewayType.Stripe;
const string braintreeCustomerId = "1";
var stripeCustomer = new S.Customer
{
Metadata = new Dictionary<string, string>
{
{ "btCustomerId", braintreeCustomerId }
}
};
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<S.CustomerGetOptions>())
.Returns(stripeCustomer);
var (_, customerGateway, paymentMethodGateway) = Setup(sutProvider.GetDependency<BT.IBraintreeGateway>());
var braintreeCustomer = Substitute.For<BT.Customer>();
braintreeCustomer.PaymentMethods.Returns(Array.Empty<BT.PaymentMethod>());
customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);
await sutProvider.Sut.RemovePaymentMethod(organization);
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
await customerGateway.DidNotReceiveWithAnyArgs().UpdateAsync(Arg.Any<string>(), Arg.Any<BT.CustomerRequest>());
await paymentMethodGateway.DidNotReceiveWithAnyArgs().DeleteAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Braintree_CustomerUpdateFails_ContactSupport(
Organization organization,
SutProvider<RemovePaymentMethodCommand> sutProvider)
{
organization.Gateway = GatewayType.Stripe;
const string braintreeCustomerId = "1";
const string braintreePaymentMethodToken = "TOKEN";
var stripeCustomer = new S.Customer
{
Metadata = new Dictionary<string, string>
{
{ "btCustomerId", braintreeCustomerId }
}
};
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<S.CustomerGetOptions>())
.Returns(stripeCustomer);
var (_, customerGateway, paymentMethodGateway) = Setup(sutProvider.GetDependency<BT.IBraintreeGateway>());
var braintreeCustomer = Substitute.For<BT.Customer>();
var paymentMethod = Substitute.For<BT.PaymentMethod>();
paymentMethod.Token.Returns(braintreePaymentMethodToken);
paymentMethod.IsDefault.Returns(true);
braintreeCustomer.PaymentMethods.Returns(new[]
{
paymentMethod
});
customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);
var updateBraintreeCustomerResult = Substitute.For<BT.Result<BT.Customer>>();
updateBraintreeCustomerResult.IsSuccess().Returns(false);
customerGateway.UpdateAsync(
braintreeCustomerId,
Arg.Is<BT.CustomerRequest>(request => request.DefaultPaymentMethodToken == null))
.Returns(updateBraintreeCustomerResult);
await ThrowsContactSupportAsync(() => sutProvider.Sut.RemovePaymentMethod(organization));
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
await customerGateway.Received(1).UpdateAsync(braintreeCustomerId, Arg.Is<BT.CustomerRequest>(request =>
request.DefaultPaymentMethodToken == null));
await paymentMethodGateway.DidNotReceiveWithAnyArgs().DeleteAsync(paymentMethod.Token);
await customerGateway.DidNotReceive().UpdateAsync(braintreeCustomerId, Arg.Is<BT.CustomerRequest>(request =>
request.DefaultPaymentMethodToken == paymentMethod.Token));
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Braintree_PaymentMethodDeleteFails_RollBack_ContactSupport(
Organization organization,
SutProvider<RemovePaymentMethodCommand> sutProvider)
{
organization.Gateway = GatewayType.Stripe;
const string braintreeCustomerId = "1";
const string braintreePaymentMethodToken = "TOKEN";
var stripeCustomer = new S.Customer
{
Metadata = new Dictionary<string, string>
{
{ "btCustomerId", braintreeCustomerId }
}
};
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<S.CustomerGetOptions>())
.Returns(stripeCustomer);
var (_, customerGateway, paymentMethodGateway) = Setup(sutProvider.GetDependency<BT.IBraintreeGateway>());
var braintreeCustomer = Substitute.For<BT.Customer>();
var paymentMethod = Substitute.For<BT.PaymentMethod>();
paymentMethod.Token.Returns(braintreePaymentMethodToken);
paymentMethod.IsDefault.Returns(true);
braintreeCustomer.PaymentMethods.Returns(new[]
{
paymentMethod
});
customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);
var updateBraintreeCustomerResult = Substitute.For<BT.Result<BT.Customer>>();
updateBraintreeCustomerResult.IsSuccess().Returns(true);
customerGateway.UpdateAsync(braintreeCustomerId, Arg.Any<BT.CustomerRequest>())
.Returns(updateBraintreeCustomerResult);
var deleteBraintreePaymentMethodResult = Substitute.For<BT.Result<BT.PaymentMethod>>();
deleteBraintreePaymentMethodResult.IsSuccess().Returns(false);
paymentMethodGateway.DeleteAsync(paymentMethod.Token).Returns(deleteBraintreePaymentMethodResult);
await ThrowsContactSupportAsync(() => sutProvider.Sut.RemovePaymentMethod(organization));
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
await customerGateway.Received(1).UpdateAsync(braintreeCustomerId, Arg.Is<BT.CustomerRequest>(request =>
request.DefaultPaymentMethodToken == null));
await paymentMethodGateway.Received(1).DeleteAsync(paymentMethod.Token);
await customerGateway.Received(1).UpdateAsync(braintreeCustomerId, Arg.Is<BT.CustomerRequest>(request =>
request.DefaultPaymentMethodToken == paymentMethod.Token));
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Stripe_Legacy_RemovesSources(
Organization organization,
SutProvider<RemovePaymentMethodCommand> sutProvider)
{
organization.Gateway = GatewayType.Stripe;
const string bankAccountId = "bank_account_id";
const string cardId = "card_id";
var sources = new List<S.IPaymentSource>
{
new S.BankAccount { Id = bankAccountId }, new S.Card { Id = cardId }
};
var stripeCustomer = new S.Customer { Sources = new S.StripeList<S.IPaymentSource> { Data = sources } };
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<S.CustomerGetOptions>())
.Returns(stripeCustomer);
stripeAdapter
.PaymentMethodListAutoPagingAsync(Arg.Any<S.PaymentMethodListOptions>())
.Returns(GetPaymentMethodsAsync(new List<S.PaymentMethod>()));
await sutProvider.Sut.RemovePaymentMethod(organization);
await stripeAdapter.Received(1).BankAccountDeleteAsync(stripeCustomer.Id, bankAccountId);
await stripeAdapter.Received(1).CardDeleteAsync(stripeCustomer.Id, cardId);
await stripeAdapter.DidNotReceiveWithAnyArgs()
.PaymentMethodDetachAsync(Arg.Any<string>(), Arg.Any<S.PaymentMethodDetachOptions>());
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Stripe_DetachesPaymentMethods(
Organization organization,
SutProvider<RemovePaymentMethodCommand> sutProvider)
{
organization.Gateway = GatewayType.Stripe;
const string bankAccountId = "bank_account_id";
const string cardId = "card_id";
var sources = new List<S.IPaymentSource>();
var stripeCustomer = new S.Customer { Sources = new S.StripeList<S.IPaymentSource> { Data = sources } };
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<S.CustomerGetOptions>())
.Returns(stripeCustomer);
stripeAdapter
.PaymentMethodListAutoPagingAsync(Arg.Any<S.PaymentMethodListOptions>())
.Returns(GetPaymentMethodsAsync(new List<S.PaymentMethod>
{
new ()
{
Id = bankAccountId
},
new ()
{
Id = cardId
}
}));
await sutProvider.Sut.RemovePaymentMethod(organization);
await stripeAdapter.DidNotReceiveWithAnyArgs().BankAccountDeleteAsync(Arg.Any<string>(), Arg.Any<string>());
await stripeAdapter.DidNotReceiveWithAnyArgs().CardDeleteAsync(Arg.Any<string>(), Arg.Any<string>());
await stripeAdapter.Received(1)
.PaymentMethodDetachAsync(bankAccountId, Arg.Any<S.PaymentMethodDetachOptions>());
await stripeAdapter.Received(1)
.PaymentMethodDetachAsync(cardId, Arg.Any<S.PaymentMethodDetachOptions>());
}
private static async IAsyncEnumerable<S.PaymentMethod> GetPaymentMethodsAsync(
IEnumerable<S.PaymentMethod> paymentMethods)
{
foreach (var paymentMethod in paymentMethods)
{
yield return paymentMethod;
}
await Task.CompletedTask;
}
private static (BT.IBraintreeGateway, BT.ICustomerGateway, BT.IPaymentMethodGateway) Setup(
BT.IBraintreeGateway braintreeGateway)
{
var customerGateway = Substitute.For<BT.ICustomerGateway>();
var paymentMethodGateway = Substitute.For<BT.IPaymentMethodGateway>();
braintreeGateway.Customer.Returns(customerGateway);
braintreeGateway.PaymentMethod.Returns(paymentMethodGateway);
return (braintreeGateway, customerGateway, paymentMethodGateway);
}
private static async Task ThrowsContactSupportAsync(Func<Task> function)
{
const string message = "Could not remove your payment method. Please contact support for assistance.";
var exception = await Assert.ThrowsAsync<GatewayException>(function);
Assert.Equal(message, exception.Message);
}
}

View File

@ -0,0 +1,100 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.OrganizationFeatures.OrganizationLicenses;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using NSubstitute;
using Xunit;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace Bit.Core.Test.OrganizationFeatures.OrganizationLicenses;
[SutProviderCustomize]
public class UpdateOrganizationLicenseCommandTests
{
private static string LicenseDirectory => Path.GetDirectoryName(OrganizationLicenseDirectory.Value);
private static Lazy<string> OrganizationLicenseDirectory => new(() =>
{
// Create a temporary directory to write the license file to
var directory = Path.Combine(Path.GetTempPath(), "bitwarden/");
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
return directory;
});
[Theory, BitAutoData]
public async Task UpdateLicenseAsync_UpdatesLicenseFileAndOrganization(
SelfHostedOrganizationDetails selfHostedOrg,
OrganizationLicense license,
SutProvider<UpdateOrganizationLicenseCommand> sutProvider)
{
var globalSettings = sutProvider.GetDependency<IGlobalSettings>();
globalSettings.LicenseDirectory = LicenseDirectory;
globalSettings.SelfHosted = true;
// Passing values for OrganizationLicense.CanUse
// NSubstitute cannot override non-virtual members so we have to ensure the real method passes
license.Enabled = true;
license.Issued = DateTime.Now.AddDays(-1);
license.Expires = DateTime.Now.AddDays(1);
license.Version = OrganizationLicense.CurrentLicenseFileVersion;
license.InstallationId = globalSettings.Installation.Id;
license.LicenseType = LicenseType.Organization;
sutProvider.GetDependency<ILicensingService>().VerifyLicense(license).Returns(true);
// Passing values for SelfHostedOrganizationDetails.CanUseLicense
// NSubstitute cannot override non-virtual members so we have to ensure the real method passes
license.Seats = null;
license.MaxCollections = null;
license.UseGroups = true;
license.UsePolicies = true;
license.UseSso = true;
license.UseKeyConnector = true;
license.UseScim = true;
license.UseCustomPermissions = true;
license.UseResetPassword = true;
try
{
await sutProvider.Sut.UpdateLicenseAsync(selfHostedOrg, license, null);
// Assertion: should have saved the license file to disk
var filePath = Path.Combine(LicenseDirectory, "organization", $"{selfHostedOrg.Id}.json");
await using var fs = File.OpenRead(filePath);
var licenseFromFile = await JsonSerializer.DeserializeAsync<OrganizationLicense>(fs);
AssertHelper.AssertPropertyEqual(license, licenseFromFile, "SignatureBytes");
// Assertion: should have updated and saved the organization
// Properties excluded from the comparison below are exceptions to the rule that the Organization mirrors
// the OrganizationLicense
await sutProvider.GetDependency<IOrganizationService>()
.Received(1)
.ReplaceAndUpdateCacheAsync(Arg.Is<Organization>(
org => AssertPropertyEqual(license, org,
"Id", "MaxStorageGb", "Issued", "Refresh", "Version", "Trial", "LicenseType",
"Hash", "Signature", "SignatureBytes", "InstallationId", "Expires", "ExpirationWithoutGracePeriod") &&
// Same property but different name, use explicit mapping
org.ExpirationDate == license.Expires));
}
finally
{
// Clean up temporary directory
Directory.Delete(OrganizationLicenseDirectory.Value, true);
}
}
// Wrapper to compare 2 objects that are different types
private bool AssertPropertyEqual(OrganizationLicense expected, Organization actual, params string[] excludedPropertyStrings)
{
AssertHelper.AssertPropertyEqual(expected, actual, excludedPropertyStrings);
return true;
}
}

View File

@ -1,5 +1,4 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
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;
@ -36,6 +35,7 @@ public class CipherServiceTests
SutProvider<CipherService> sutProvider) SutProvider<CipherService> sutProvider)
{ {
organization.MaxCollections = null; organization.MaxCollections = null;
organization.FlexibleCollections = false;
importingOrganizationUser.OrganizationId = organization.Id; importingOrganizationUser.OrganizationId = organization.Id;
foreach (var collection in collections) foreach (var collection in collections)
@ -62,10 +62,6 @@ public class CipherServiceTests
.GetByOrganizationAsync(organization.Id, importingUserId) .GetByOrganizationAsync(organization.Id, importingUserId)
.Returns(importingOrganizationUser); .Returns(importingOrganizationUser);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.FlexibleCollections, Arg.Any<ICurrentContext>(), Arg.Any<bool>())
.Returns(false);
// Set up a collection that already exists in the organization // Set up a collection that already exists in the organization
sutProvider.GetDependency<ICollectionRepository>() sutProvider.GetDependency<ICollectionRepository>()
.GetManyByOrganizationIdAsync(organization.Id) .GetManyByOrganizationIdAsync(organization.Id)
@ -95,6 +91,7 @@ public class CipherServiceTests
SutProvider<CipherService> sutProvider) SutProvider<CipherService> sutProvider)
{ {
organization.MaxCollections = null; organization.MaxCollections = null;
organization.FlexibleCollections = true;
importingOrganizationUser.OrganizationId = organization.Id; importingOrganizationUser.OrganizationId = organization.Id;
foreach (var collection in collections) foreach (var collection in collections)
@ -121,10 +118,6 @@ public class CipherServiceTests
.GetByOrganizationAsync(organization.Id, importingUserId) .GetByOrganizationAsync(organization.Id, importingUserId)
.Returns(importingOrganizationUser); .Returns(importingOrganizationUser);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.FlexibleCollections, Arg.Any<ICurrentContext>(), Arg.Any<bool>())
.Returns(true);
// Set up a collection that already exists in the organization // Set up a collection that already exists in the organization
sutProvider.GetDependency<ICollectionRepository>() sutProvider.GetDependency<ICollectionRepository>()
.GetManyByOrganizationIdAsync(organization.Id) .GetManyByOrganizationIdAsync(organization.Id)

View File

@ -72,7 +72,22 @@ public partial class GrantIdWithIndexes : Migration
.Annotation("MySql:CharSet", "utf8mb4") .Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4"); .OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.Sql("ALTER TABLE `Grant` ADD COLUMN `Id` INT AUTO_INCREMENT UNIQUE;"); migrationBuilder.Sql(@"
DROP PROCEDURE IF EXISTS GrantSchemaChange;
CREATE PROCEDURE GrantSchemaChange()
BEGIN
IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Grant' AND COLUMN_NAME = 'Id') THEN
ALTER TABLE `Grant` DROP COLUMN `Id`;
END IF;
ALTER TABLE `Grant` ADD COLUMN `Id` INT AUTO_INCREMENT UNIQUE;
END;
CALL GrantSchemaChange();
DROP PROCEDURE GrantSchemaChange;"
);
migrationBuilder.AddPrimaryKey( migrationBuilder.AddPrimaryKey(
name: "PK_Grant", name: "PK_Grant",

View File

@ -10,7 +10,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.14"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.15">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -6,7 +6,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.14"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.15">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.14"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.15">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -0,0 +1,45 @@
ALTER TABLE
"Grant" RENAME TO "Old_Grant";
CREATE TABLE "Grant"
(
"Key" TEXT NOT NULL CONSTRAINT "PK_Grant" PRIMARY KEY,
"Type" TEXT NULL,
"SubjectId" TEXT NULL,
"SessionId" TEXT NULL,
"ClientId" TEXT NULL,
"Description" TEXT NULL,
"CreationDate" TEXT NOT NULL,
"ExpirationDate" TEXT NULL,
"ConsumedDate" TEXT NULL,
"Data" TEXT NULL
);
INSERT INTO
"Grant"
(
"Key",
"Type",
"SubjectId",
"SessionId",
"ClientId",
"Description",
"CreationDate",
"ExpirationDate",
"ConsumedDate",
"Data"
)
SELECT
"Key",
"Type",
"SubjectId",
"SessionId",
"ClientId",
"Description",
"CreationDate",
"ExpirationDate",
"ConsumedDate",
"Data"
FROM "Old_Grant";
DROP TABLE "Old_Grant";

View File

@ -0,0 +1,46 @@
ALTER TABLE
"Grant" RENAME TO "Old_Grant";
CREATE TABLE "Grant"
(
"Id" INTEGER PRIMARY KEY AUTOINCREMENT,
"Key" TEXT NOT NULL,
"Type" TEXT NOT NULL,
"SubjectId" TEXT NULL,
"SessionId" TEXT NULL,
"ClientId" TEXT NOT NULL,
"Description" TEXT NULL,
"CreationDate" TEXT NOT NULL,
"ExpirationDate" TEXT NULL,
"ConsumedDate" TEXT NULL,
"Data" TEXT NOT NULL
);
INSERT INTO
"Grant"
(
"Key",
"Type",
"SubjectId",
"SessionId",
"ClientId",
"Description",
"CreationDate",
"ExpirationDate",
"ConsumedDate",
"Data"
)
SELECT
"Key",
"Type",
"SubjectId",
"SessionId",
"ClientId",
"Description",
"CreationDate",
"ExpirationDate",
"ConsumedDate",
"Data"
FROM "Old_Grant";
DROP TABLE "Old_Grant";

View File

@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore.Migrations; using Bit.EfShared;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable #nullable disable
@ -7,59 +8,12 @@ namespace Bit.SqliteMigrations.Migrations;
/// <inheritdoc /> /// <inheritdoc />
public partial class GrantIdWithIndexes : Migration public partial class GrantIdWithIndexes : Migration
{ {
private const string _scriptLocationTemplate = "2023-12-04_00_{0}_GrantIndexes.sql";
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropPrimaryKey( migrationBuilder.SqlResource(_scriptLocationTemplate);
name: "PK_Grant",
table: "Grant");
migrationBuilder.AlterColumn<string>(
name: "Type",
table: "Grant",
type: "TEXT",
maxLength: 50,
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "TEXT",
oldMaxLength: 50,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Data",
table: "Grant",
type: "TEXT",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "ClientId",
table: "Grant",
type: "TEXT",
maxLength: 200,
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "TEXT",
oldMaxLength: 200,
oldNullable: true);
migrationBuilder.AddColumn<int>(
name: "Id",
table: "Grant",
type: "INTEGER",
nullable: false,
defaultValue: 0)
.Annotation("Sqlite:Autoincrement", true);
migrationBuilder.AddPrimaryKey(
name: "PK_Grant",
table: "Grant",
column: "Id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Grant_Key", name: "IX_Grant_Key",
@ -71,49 +25,10 @@ public partial class GrantIdWithIndexes : Migration
/// <inheritdoc /> /// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropPrimaryKey( migrationBuilder.SqlResource(_scriptLocationTemplate);
name: "PK_Grant",
table: "Grant");
migrationBuilder.DropIndex( migrationBuilder.DropIndex(
name: "IX_Grant_Key", name: "IX_Grant_Key",
table: "Grant"); table: "Grant");
migrationBuilder.DropColumn(
name: "Id",
table: "Grant");
migrationBuilder.AlterColumn<string>(
name: "Type",
table: "Grant",
type: "TEXT",
maxLength: 50,
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldMaxLength: 50);
migrationBuilder.AlterColumn<string>(
name: "Data",
table: "Grant",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "ClientId",
table: "Grant",
type: "TEXT",
maxLength: 200,
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldMaxLength: 200);
migrationBuilder.AddPrimaryKey(
name: "PK_Grant",
table: "Grant",
column: "Key");
} }
} }

View File

@ -12,7 +12,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.14"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.15">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
@ -22,4 +22,9 @@
<Compile Include="..\EfShared\MigrationBuilderExtensions.cs" /> <Compile Include="..\EfShared\MigrationBuilderExtensions.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<EmbeddedResource Include="HelperScripts\2023-12-04_00_Up_GrantIndexes.sql" />
<EmbeddedResource Include="HelperScripts\2023-12-04_00_Down_GrantIndexes.sql" />
</ItemGroup>
</Project> </Project>