1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-20 11:04:31 -05:00

[AC-1707] Restrict provider access to items (#3881)

* [AC-2274] Introduce CanEditAnyCiphersAsAdminAsync helper to replace EditAnyCollection usage

* [AC-2274] Add unit tests for CanEditAnyCiphersAsAdmin helper

* [AC-2274] Add Jira ticket

* [AC-1707] Add feature flag

* [AC-1707] Update CanEditAnyCiphersAsAdmin to fail for providers when the feature flag is enabled

* [AC-2274] Undo change to purge endpoint

* [AC-2274] Update admin checks to account for unassigned ciphers

* [AC-1707] Fix provider auth checks after merge with main

* [AC-1707] Fix tests after merge

* [AC-1707] Adjust CanEditCipherAsAdmin method to properly account for admin user types

- Fix associated unit tests

* [AC-1707] Formatting
This commit is contained in:
Shane Melton 2024-05-07 12:30:48 -07:00 committed by GitHub
parent 1ede40d5e1
commit 45be4d5069
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 52 additions and 38 deletions

View File

@ -334,12 +334,32 @@ public class CiphersController : Controller
return await _currentContext.EditAnyCollection(organizationId); return await _currentContext.EditAnyCollection(organizationId);
} }
if (await CanEditCiphersAsync(organizationId, cipherIds)) var org = _currentContext.GetOrganization(organizationId);
// If we're not an "admin", we don't need to check the ciphers
if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true }))
{ {
return true; // Are we a provider user? If so, we need to be sure we're not restricted
// Once the feature flag is removed, this check can be combined with the above
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
{
// Provider is restricted from editing ciphers, so we're not an "admin"
if (_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess))
{
return false;
}
// Provider is unrestricted, so we're an "admin", don't return early
}
else
{
// Not a provider or admin
return false;
}
} }
return false; // We know we're an "admin", now check the ciphers explicitly (in case admins are restricted)
return await CanEditCiphersAsync(organizationId, cipherIds);
} }
/// <summary> /// <summary>
@ -360,10 +380,10 @@ public class CiphersController : Controller
return true; return true;
} }
// Provider users can access all ciphers in V1 (to change later) // Provider users can only access all ciphers if RestrictProviderAccess is disabled
if (await _currentContext.ProviderUserForOrgAsync(organizationId)) if (await _currentContext.ProviderUserForOrgAsync(organizationId))
{ {
return true; return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess);
} }
return false; return false;
@ -399,10 +419,10 @@ public class CiphersController : Controller
return true; return true;
} }
// Provider users can edit all ciphers in V1 (to change later) // Provider users can edit all ciphers if RestrictProviderAccess is disabled
if (await _currentContext.ProviderUserForOrgAsync(organizationId)) if (await _currentContext.ProviderUserForOrgAsync(organizationId))
{ {
return true; return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess);
} }
return false; return false;
@ -422,10 +442,10 @@ public class CiphersController : Controller
return true; return true;
} }
// Provider users can still access organization ciphers in V1 (to change later) // Provider users can only access organization ciphers if RestrictProviderAccess is disabled
if (await _currentContext.ProviderUserForOrgAsync(organizationId)) if (await _currentContext.ProviderUserForOrgAsync(organizationId))
{ {
return true; return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess);
} }
return false; return false;
@ -445,10 +465,10 @@ public class CiphersController : Controller
return true; return true;
} }
// Provider users can access all ciphers in V1 (to change later) // Provider users can only access all ciphers if RestrictProviderAccess is disabled
if (await _currentContext.ProviderUserForOrgAsync(organizationId)) if (await _currentContext.ProviderUserForOrgAsync(organizationId))
{ {
return true; return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess);
} }
return false; return false;

View File

@ -139,6 +139,7 @@ public static class FeatureFlagKeys
public const string EmailVerification = "email-verification"; public const string EmailVerification = "email-verification";
public const string AnhFcmv1Migration = "anh-fcmv1-migration"; public const string AnhFcmv1Migration = "anh-fcmv1-migration";
public const string ExtensionRefresh = "extension-refresh"; public const string ExtensionRefresh = "extension-refresh";
public const string RestrictProviderAccess = "restrict-provider-access";
public static List<string> GetAllKeys() public static List<string> GetAllKeys()
{ {

View File

@ -1,6 +1,5 @@
using System.Security.Claims; using System.Security.Claims;
using Bit.Api.Vault.Controllers; using Bit.Api.Vault.Controllers;
using Bit.Api.Vault.Models;
using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Request;
using Bit.Core; using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
@ -9,7 +8,6 @@ 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 Bit.Core.Vault.Entities; using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Enums;
using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories; using Bit.Core.Vault.Repositories;
using Bit.Core.Vault.Services; using Bit.Core.Vault.Services;
@ -62,9 +60,10 @@ public class CiphersControllerTests
[BitAutoData(OrganizationUserType.Custom, false, false)] [BitAutoData(OrganizationUserType.Custom, false, false)]
public async Task CanEditCiphersAsAdminAsync_FlexibleCollections_Success( public async Task CanEditCiphersAsAdminAsync_FlexibleCollections_Success(
OrganizationUserType userType, bool allowAdminsAccessToAllItems, bool shouldSucceed, OrganizationUserType userType, bool allowAdminsAccessToAllItems, bool shouldSucceed,
CurrentContextOrganization organization, Guid userId, SutProvider<CiphersController> sutProvider CurrentContextOrganization organization, Guid userId, Cipher cipher, SutProvider<CiphersController> sutProvider
) )
{ {
cipher.OrganizationId = organization.Id;
organization.Type = userType; organization.Type = userType;
if (userType == OrganizationUserType.Custom) if (userType == OrganizationUserType.Custom)
{ {
@ -74,6 +73,9 @@ public class CiphersControllerTests
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipher.Id).Returns(cipher);
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipher });
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility
{ {
Id = organization.Id, Id = organization.Id,
@ -82,23 +84,17 @@ public class CiphersControllerTests
}); });
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true); sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true);
var requestModel = new CipherCreateRequestModel
{
Cipher = new CipherRequestModel { OrganizationId = organization.Id.ToString(), Type = CipherType.Login, Login = new CipherLoginModel() },
CollectionIds = new List<Guid>()
};
if (shouldSucceed) if (shouldSucceed)
{ {
await sutProvider.Sut.PostAdmin(requestModel); await sutProvider.Sut.DeleteAdmin(cipher.Id.ToString());
await sutProvider.GetDependency<ICipherService>().ReceivedWithAnyArgs() await sutProvider.GetDependency<ICipherService>().ReceivedWithAnyArgs()
.SaveAsync(default, default, default); .DeleteAsync(default, default);
} }
else else
{ {
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PostAdmin(requestModel)); await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipher.Id.ToString()));
await sutProvider.GetDependency<ICipherService>().DidNotReceiveWithAnyArgs() await sutProvider.GetDependency<ICipherService>().DidNotReceiveWithAnyArgs()
.SaveAsync(default, default, default); .DeleteAsync(default, default);
} }
} }
@ -144,24 +140,19 @@ public class CiphersControllerTests
} }
[Theory] [Theory]
[BitAutoData(false, true)]
[BitAutoData(true, true)]
[BitAutoData(false, false)] [BitAutoData(false, false)]
[BitAutoData(true, false)] [BitAutoData(true, false)]
[BitAutoData(true, true)]
public async Task CanEditCiphersAsAdminAsync_Providers( public async Task CanEditCiphersAsAdminAsync_Providers(
bool fcV1Enabled, bool shouldSucceed, Cipher cipher, CurrentContextOrganization organization, Guid userId, SutProvider<CiphersController> sutProvider bool fcV1Enabled, bool restrictProviders, Cipher cipher, CurrentContextOrganization organization, Guid userId, SutProvider<CiphersController> sutProvider
) )
{ {
cipher.OrganizationId = organization.Id; cipher.OrganizationId = organization.Id;
if (fcV1Enabled)
{ // Simulate that the user is a provider for the organization
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(organization.Id).Returns(shouldSucceed); sutProvider.GetDependency<ICurrentContext>().EditAnyCollection(organization.Id).Returns(true);
} sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(organization.Id).Returns(true);
else
{
sutProvider.GetDependency<ICurrentContext>().EditAnyCollection(organization.Id).Returns(shouldSucceed);
}
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipher.Id).Returns(cipher); sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipher.Id).Returns(cipher);
@ -174,14 +165,16 @@ public class CiphersControllerTests
AllowAdminAccessToAllCollectionItems = false AllowAdminAccessToAllCollectionItems = false
}); });
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(fcV1Enabled); sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(fcV1Enabled);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(restrictProviders);
if (shouldSucceed) // Non V1 FC or non restricted providers should succeed
if (!fcV1Enabled || !restrictProviders)
{ {
await sutProvider.Sut.DeleteAdmin(cipher.Id.ToString()); await sutProvider.Sut.DeleteAdmin(cipher.Id.ToString());
await sutProvider.GetDependency<ICipherService>().ReceivedWithAnyArgs() await sutProvider.GetDependency<ICipherService>().ReceivedWithAnyArgs()
.DeleteAsync(default, default); .DeleteAsync(default, default);
} }
else else // Otherwise, they should fail
{ {
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipher.Id.ToString())); await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipher.Id.ToString()));
await sutProvider.GetDependency<ICipherService>().DidNotReceiveWithAnyArgs() await sutProvider.GetDependency<ICipherService>().DidNotReceiveWithAnyArgs()