1
0
mirror of https://github.com/bitwarden/server synced 2026-02-11 22:13:24 +00:00

[PM-22103] Exclude default collections from admin apis (#6021)

* feat: exclude DefaultUserCollection from GetManyByOrganizationIdWithPermissionsAsync

Updated EF implementation, SQL procedure, and unit test to verify that default user collections are filtered from results

* Update the public CollectionsController.Get method to return a NotFoundResult for collections of type DefaultUserCollection.

* Add unit tests for the public CollectionsController

* Update ICollectionRepository.GetManyByOrganizationIdAsync to exclude results of the type DefaultUserCollection

Modified the SQL stored procedure and the EF query to reflect this change and added a new integration test to ensure the functionality works as expected.

* Refactor CollectionsController to remove unused IApplicationCacheService dependency

* Update IOrganizationUserRepository.GetDetailsByIdWithCollectionsAsync to exclude DefaultUserCollections

* Update IOrganizationUserRepository.GetManyDetailsByOrganizationAsync to exclude DefaultUserCollections

* Undo change to GetByIdWithCollectionsAsync

* Update integration test to verify exclusion of DefaultUserCollection in OrganizationUserRepository.GetDetailsByIdWithCollectionsAsync

* Clarify documentation in ICollectionRepository to specify that GetManyByOrganizationIdWithAccessAsync returns only shared collections belonging to the organization.

* Add Arrange, Act, and Assert comments to CollectionsControllerTests
This commit is contained in:
Rui Tomé
2025-07-18 13:00:54 +01:00
committed by GitHub
parent 828003f101
commit 30300bc59b
14 changed files with 500 additions and 15 deletions

View File

@@ -8,7 +8,6 @@ using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -21,18 +20,15 @@ public class CollectionsController : Controller
private readonly ICollectionRepository _collectionRepository;
private readonly IUpdateCollectionCommand _updateCollectionCommand;
private readonly ICurrentContext _currentContext;
private readonly IApplicationCacheService _applicationCacheService;
public CollectionsController(
ICollectionRepository collectionRepository,
IUpdateCollectionCommand updateCollectionCommand,
ICurrentContext currentContext,
IApplicationCacheService applicationCacheService)
ICurrentContext currentContext)
{
_collectionRepository = collectionRepository;
_updateCollectionCommand = updateCollectionCommand;
_currentContext = currentContext;
_applicationCacheService = applicationCacheService;
}
/// <summary>
@@ -49,7 +45,8 @@ public class CollectionsController : Controller
public async Task<IActionResult> Get(Guid id)
{
(var collection, var access) = await _collectionRepository.GetByIdWithAccessAsync(id);
if (collection == null || collection.OrganizationId != _currentContext.OrganizationId)
if (collection == null || collection.OrganizationId != _currentContext.OrganizationId ||
collection.Type == CollectionType.DefaultUserCollection)
{
return new NotFoundResult();
}

View File

@@ -22,7 +22,19 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);
Task<Tuple<OrganizationUser?, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);
Task<OrganizationUserUserDetails?> GetDetailsByIdAsync(Guid id);
/// <summary>
/// Returns the OrganizationUser and its associated collections (excluding DefaultUserCollections).
/// </summary>
/// <param name="id">The id of the OrganizationUser</param>
/// <returns>A tuple containing the OrganizationUser and its associated collections</returns>
Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithCollectionsAsync(Guid id);
/// <summary>
/// Returns the OrganizationUsers and their associated collections (excluding DefaultUserCollections).
/// </summary>
/// <param name="organizationId">The id of the organization</param>
/// <param name="includeGroups">Whether to include groups</param>
/// <param name="includeCollections">Whether to include collections</param>
/// <returns>A list of OrganizationUserUserDetails</returns>
Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups = false, bool includeCollections = false);
Task<ICollection<OrganizationUserOrganizationDetails>> GetManyDetailsByUserAsync(Guid userId,
OrganizationUserStatusType? status = null);

View File

@@ -16,12 +16,12 @@ public interface ICollectionRepository : IRepository<Collection, Guid>
/// <summary>
/// Return all collections that belong to the organization. Does not include any permission details or group/user
/// access relationships.
/// access relationships. Excludes default collections (My Items collections).
/// </summary>
Task<ICollection<Collection>> GetManyByOrganizationIdAsync(Guid organizationId);
/// <summary>
/// Return all collections that belong to the organization. Includes group/user access relationships for each collection.
/// Return all shared collections that belong to the organization. Includes group/user access relationships for each collection.
/// </summary>
Task<ICollection<Tuple<Collection, CollectionAccessDetails>>> GetManyByOrganizationIdWithAccessAsync(Guid organizationId);
@@ -34,9 +34,10 @@ public interface ICollectionRepository : IRepository<Collection, Guid>
Task<ICollection<CollectionDetails>> GetManyByUserIdAsync(Guid userId);
/// <summary>
/// Returns all collections for an organization, including permission info for the specified user.
/// Returns all shared collections for an organization, including permission info for the specified user.
/// This does not perform any authorization checks internally!
/// Optionally, you can include access relationships for other Groups/Users and the collections.
/// Excludes default collections (My Items collections) - used by Admin Console Collections tab.
/// </summary>
Task<ICollection<CollectionAdminDetails>> GetManyByOrganizationIdWithPermissionsAsync(Guid organizationId, Guid userId, bool includeAccessRelationships);

View File

@@ -257,7 +257,8 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
var dbContext = GetDatabaseContext(scope);
var query = from ou in dbContext.OrganizationUsers
join cu in dbContext.CollectionUsers on ou.Id equals cu.OrganizationUserId
where ou.Id == id
join c in dbContext.Collections on cu.CollectionId equals c.Id
where ou.Id == id && c.Type != CollectionType.DefaultUserCollection
select cu;
var collections = await query.Select(cu => new CollectionAccessSelection
{
@@ -369,6 +370,8 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
{
collections = (await (from cu in dbContext.CollectionUsers
join ou in userIdEntities on cu.OrganizationUserId equals ou.Id
join c in dbContext.Collections on cu.CollectionId equals c.Id
where c.Type != CollectionType.DefaultUserCollection
select cu).ToListAsync())
.GroupBy(c => c.OrganizationUserId).ToList();
}

View File

@@ -1,4 +1,5 @@
using AutoMapper;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Infrastructure.EntityFramework.Models;
@@ -216,7 +217,8 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
{
var dbContext = GetDatabaseContext(scope);
var query = from c in dbContext.Collections
where c.OrganizationId == organizationId
where c.OrganizationId == organizationId &&
c.Type != CollectionType.DefaultUserCollection
select c;
var collections = await query.ToArrayAsync();
return collections;

View File

@@ -1,4 +1,5 @@
using Bit.Core.Models.Data;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
@@ -59,7 +60,9 @@ public class CollectionAdminDetailsQuery : IQuery<CollectionAdminDetails>
if (_organizationId.HasValue)
{
baseCollectionQuery = baseCollectionQuery.Where(x => x.c.OrganizationId == _organizationId);
baseCollectionQuery = baseCollectionQuery.Where(x =>
x.c.OrganizationId == _organizationId &&
x.c.Type != CollectionType.DefaultUserCollection);
}
else if (_collectionId.HasValue)
{

View File

@@ -10,6 +10,10 @@ BEGIN
[dbo].[OrganizationUser] OU
INNER JOIN
[dbo].[CollectionUser] CU ON CU.[OrganizationUserId] = OU.[Id]
INNER JOIN
[dbo].[Collection] C ON CU.[CollectionId] = C.[Id]
INNER JOIN
@OrganizationUserIds OUI ON OUI.[Id] = OU.[Id]
WHERE
C.[Type] != 1 -- Exclude DefaultUserCollection
END

View File

@@ -9,5 +9,6 @@ BEGIN
FROM
[dbo].[CollectionView]
WHERE
[OrganizationId] = @OrganizationId
[OrganizationId] = @OrganizationId AND
[Type] != 1 -- Exclude DefaultUserCollection
END

View File

@@ -15,6 +15,9 @@ BEGIN
[dbo].[OrganizationUser] OU
INNER JOIN
[dbo].[CollectionUser] CU ON CU.[OrganizationUserId] = [OU].[Id]
INNER JOIN
[dbo].[Collection] C ON CU.[CollectionId] = C.[Id]
WHERE
[OrganizationUserId] = @Id
AND C.[Type] != 1 -- Exclude default user collections
END

View File

@@ -66,7 +66,8 @@ BEGIN
LEFT JOIN
[dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId]
WHERE
C.[OrganizationId] = @OrganizationId
C.[OrganizationId] = @OrganizationId AND
C.[Type] != 1 -- Exclude DefaultUserCollection
GROUP BY
C.[Id],
C.[OrganizationId],