mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05:00
[AC-2172] Member modal - limit admin access (#3934)
* update OrganizationUsersController PUT and POST * enforces new collection access checks when updating members * refactor BulkCollectionAuthorizationHandler to avoid repeated db calls
This commit is contained in:
@ -3,6 +3,7 @@ using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
@ -186,16 +187,28 @@ public class OrganizationUsersController : Controller
|
||||
}
|
||||
|
||||
[HttpPost("invite")]
|
||||
public async Task Invite(string orgId, [FromBody] OrganizationUserInviteRequestModel model)
|
||||
public async Task Invite(Guid orgId, [FromBody] OrganizationUserInviteRequestModel model)
|
||||
{
|
||||
var orgGuidId = new Guid(orgId);
|
||||
if (!await _currentContext.ManageUsers(orgGuidId))
|
||||
if (!await _currentContext.ManageUsers(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Flexible Collections - check the user has permission to grant access to the collections for the new user
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId) && _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1))
|
||||
{
|
||||
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Collections.Select(a => a.Id));
|
||||
var authorized =
|
||||
(await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.ModifyAccess))
|
||||
.Succeeded;
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException("You are not authorized to grant access to these collections.");
|
||||
}
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
var result = await _organizationService.InviteUsersAsync(orgGuidId, userId.Value,
|
||||
await _organizationService.InviteUsersAsync(orgId, userId.Value,
|
||||
new (OrganizationUserInvite, string)[] { (new OrganizationUserInvite(model.ToData()), null) });
|
||||
}
|
||||
|
||||
@ -316,6 +329,35 @@ public class OrganizationUsersController : Controller
|
||||
[HttpPut("{id}")]
|
||||
[HttpPost("{id}")]
|
||||
public async Task Put(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model)
|
||||
{
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId) && _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1))
|
||||
{
|
||||
// Use new Flexible Collections v1 logic
|
||||
await Put_vNext(orgId, id, model);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-Flexible Collections v1 code follows
|
||||
if (!await _currentContext.ManageUsers(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organizationUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||
if (organizationUser == null || organizationUser.OrganizationId != orgId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
await _updateOrganizationUserCommand.UpdateUserAsync(model.ToOrganizationUser(organizationUser), userId.Value,
|
||||
model.Collections.Select(c => c.ToSelectionReadOnly()).ToList(), model.Groups);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Put logic for Flexible Collections v1
|
||||
/// </summary>
|
||||
private async Task Put_vNext(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model)
|
||||
{
|
||||
if (!await _currentContext.ManageUsers(orgId))
|
||||
{
|
||||
@ -332,17 +374,44 @@ public class OrganizationUsersController : Controller
|
||||
// In this case we just don't update groups
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId);
|
||||
var restrictEditingGroups = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1) &&
|
||||
organizationAbility.FlexibleCollections &&
|
||||
userId == organizationUser.UserId &&
|
||||
!organizationAbility.AllowAdminAccessToAllCollectionItems;
|
||||
var editingSelf = userId == organizationUser.UserId;
|
||||
|
||||
var groups = restrictEditingGroups
|
||||
var groups = editingSelf && !organizationAbility.AllowAdminAccessToAllCollectionItems
|
||||
? null
|
||||
: model.Groups;
|
||||
|
||||
// The client only sends collections that the saving user has permissions to edit.
|
||||
// On the server side, we need to (1) confirm this and (2) concat these with the collections that the user
|
||||
// can't edit before saving to the database.
|
||||
var (_, currentAccess) = await _organizationUserRepository.GetByIdWithCollectionsAsync(id);
|
||||
var currentCollections = await _collectionRepository
|
||||
.GetManyByManyIdsAsync(currentAccess.Select(cas => cas.Id));
|
||||
|
||||
var readonlyCollectionIds = new HashSet<Guid>();
|
||||
foreach (var collection in currentCollections)
|
||||
{
|
||||
if (!(await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyAccess))
|
||||
.Succeeded)
|
||||
{
|
||||
readonlyCollectionIds.Add(collection.Id);
|
||||
}
|
||||
}
|
||||
|
||||
if (model.Collections.Any(c => readonlyCollectionIds.Contains(c.Id)))
|
||||
{
|
||||
throw new BadRequestException("You must have Can Manage permissions to edit a collection's membership");
|
||||
}
|
||||
|
||||
var editedCollectionAccess = model.Collections
|
||||
.Select(c => c.ToSelectionReadOnly());
|
||||
var readonlyCollectionAccess = currentAccess
|
||||
.Where(ca => readonlyCollectionIds.Contains(ca.Id));
|
||||
var collectionsToSave = editedCollectionAccess
|
||||
.Concat(readonlyCollectionAccess)
|
||||
.ToList();
|
||||
|
||||
await _updateOrganizationUserCommand.UpdateUserAsync(model.ToOrganizationUser(organizationUser), userId,
|
||||
model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), groups);
|
||||
collectionsToSave, groups);
|
||||
}
|
||||
|
||||
[HttpPut("{userId}/reset-password-enrollment")]
|
||||
|
@ -1,4 +1,5 @@
|
||||
#nullable enable
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -20,16 +21,20 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private Guid _targetOrganizationId;
|
||||
private HashSet<Guid>? _managedCollectionsIds;
|
||||
|
||||
public BulkCollectionAuthorizationHandler(
|
||||
ICurrentContext currentContext,
|
||||
ICollectionRepository collectionRepository,
|
||||
IApplicationCacheService applicationCacheService)
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_collectionRepository = collectionRepository;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
@ -129,7 +134,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
||||
// ensure they have access for the collection being read
|
||||
if (org is not null)
|
||||
{
|
||||
var canManageCollections = await CanManageCollectionsAsync(resources, org);
|
||||
var canManageCollections = await CanManageCollectionsAsync(resources);
|
||||
if (canManageCollections)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
@ -162,7 +167,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
||||
// ensure they have access with manage permission for the collection being read
|
||||
if (org is not null)
|
||||
{
|
||||
var canManageCollections = await CanManageCollectionsAsync(resources, org);
|
||||
var canManageCollections = await CanManageCollectionsAsync(resources);
|
||||
if (canManageCollections)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
@ -184,10 +189,19 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
||||
IAuthorizationRequirement requirement, ICollection<Collection> resources,
|
||||
CurrentContextOrganization? org)
|
||||
{
|
||||
// Owners, Admins, and users with EditAnyCollection permission can always manage collection access
|
||||
// Users with EditAnyCollection permission can always update a collection
|
||||
if (org is
|
||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||
{ Permissions.EditAnyCollection: true })
|
||||
{ Permissions.EditAnyCollection: true })
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
// If V1 is enabled, Owners and Admins can update any collection only if permitted by collection management settings
|
||||
var organizationAbility = await GetOrganizationAbilityAsync(org);
|
||||
var allowAdminAccessToAllCollectionItems = !_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1) ||
|
||||
organizationAbility is { AllowAdminAccessToAllCollectionItems: true };
|
||||
if (allowAdminAccessToAllCollectionItems && org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin })
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
@ -197,7 +211,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
||||
// ensure they have manage permission for the collection being managed
|
||||
if (org is not null)
|
||||
{
|
||||
var canManageCollections = await CanManageCollectionsAsync(resources, org);
|
||||
var canManageCollections = await CanManageCollectionsAsync(resources);
|
||||
if (canManageCollections)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
@ -229,7 +243,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
||||
// ensure acting user has manage permissions for all collections being deleted
|
||||
if (await GetOrganizationAbilityAsync(org) is { LimitCollectionCreationDeletion: false })
|
||||
{
|
||||
var canManageCollections = await CanManageCollectionsAsync(resources, org);
|
||||
var canManageCollections = await CanManageCollectionsAsync(resources);
|
||||
if (canManageCollections)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
@ -244,21 +258,19 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> CanManageCollectionsAsync(
|
||||
ICollection<Collection> targetCollections,
|
||||
CurrentContextOrganization org)
|
||||
private async Task<bool> CanManageCollectionsAsync(ICollection<Collection> targetCollections)
|
||||
{
|
||||
// List of collection Ids the acting user has access to
|
||||
var assignedCollectionIds =
|
||||
(await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId!.Value, useFlexibleCollections: true))
|
||||
.Where(c =>
|
||||
// Check Collections with Manage permission
|
||||
c.Manage && c.OrganizationId == org.Id)
|
||||
.Select(c => c.Id)
|
||||
.ToHashSet();
|
||||
if (_managedCollectionsIds == null)
|
||||
{
|
||||
var allUserCollections = await _collectionRepository
|
||||
.GetManyByUserIdAsync(_currentContext.UserId!.Value, useFlexibleCollections: true);
|
||||
_managedCollectionsIds = allUserCollections
|
||||
.Where(c => c.Manage)
|
||||
.Select(c => c.Id)
|
||||
.ToHashSet();
|
||||
}
|
||||
|
||||
// Check if the acting user has access to all target collections
|
||||
return targetCollections.All(tc => assignedCollectionIds.Contains(tc.Id));
|
||||
return targetCollections.All(tc => _managedCollectionsIds.Contains(tc.Id));
|
||||
}
|
||||
|
||||
private async Task<OrganizationAbility?> GetOrganizationAbilityAsync(CurrentContextOrganization? organization)
|
||||
|
@ -9,11 +9,12 @@ BEGIN
|
||||
SELECT
|
||||
CU.[CollectionId] Id,
|
||||
CU.[ReadOnly],
|
||||
CU.[HidePasswords]
|
||||
CU.[HidePasswords],
|
||||
CU.[Manage]
|
||||
FROM
|
||||
[dbo].[OrganizationUser] OU
|
||||
INNER JOIN
|
||||
[dbo].[CollectionUser] CU ON OU.[AccessAll] = 0 AND CU.[OrganizationUserId] = [OU].[Id]
|
||||
WHERE
|
||||
[OrganizationUserId] = @Id
|
||||
END
|
||||
END
|
||||
|
Reference in New Issue
Block a user