mirror of
https://github.com/bitwarden/server.git
synced 2025-07-18 16:11:28 -05: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:
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -9,5 +9,6 @@ BEGIN
|
||||
FROM
|
||||
[dbo].[CollectionView]
|
||||
WHERE
|
||||
[OrganizationId] = @OrganizationId
|
||||
[OrganizationId] = @OrganizationId AND
|
||||
[Type] != 1 -- Exclude DefaultUserCollection
|
||||
END
|
@ -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
|
||||
|
@ -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],
|
||||
|
122
test/Api.Test/Public/Controllers/CollectionsControllerTests.cs
Normal file
122
test/Api.Test/Public/Controllers/CollectionsControllerTests.cs
Normal file
@ -0,0 +1,122 @@
|
||||
using Bit.Api.Models.Public.Response;
|
||||
using Bit.Api.Public.Controllers;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.Public.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(CollectionsController))]
|
||||
[SutProviderCustomize]
|
||||
public class CollectionsControllerTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task Get_WithDefaultUserCollection_ReturnsNotFound(
|
||||
Collection collection, SutProvider<CollectionsController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
collection.Type = CollectionType.DefaultUserCollection;
|
||||
var access = new CollectionAccessDetails
|
||||
{
|
||||
Groups = new List<CollectionAccessSelection>(),
|
||||
Users = new List<CollectionAccessSelection>()
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationId.Returns(collection.OrganizationId);
|
||||
sutProvider.GetDependency<ICollectionRepository>()
|
||||
.GetByIdWithAccessAsync(collection.Id)
|
||||
.Returns(new Tuple<Collection?, CollectionAccessDetails>(collection, access));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Get(collection.Id);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<NotFoundResult>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Get_WithSharedCollection_ReturnsCollection(
|
||||
Collection collection, SutProvider<CollectionsController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
collection.Type = CollectionType.SharedCollection;
|
||||
var access = new CollectionAccessDetails
|
||||
{
|
||||
Groups = [],
|
||||
Users = []
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationId.Returns(collection.OrganizationId);
|
||||
sutProvider.GetDependency<ICollectionRepository>()
|
||||
.GetByIdWithAccessAsync(collection.Id)
|
||||
.Returns(new Tuple<Collection?, CollectionAccessDetails>(collection, access));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Get(collection.Id);
|
||||
|
||||
// Assert
|
||||
var jsonResult = Assert.IsType<JsonResult>(result);
|
||||
var response = Assert.IsType<CollectionResponseModel>(jsonResult.Value);
|
||||
Assert.Equal(collection.Id, response.Id);
|
||||
Assert.Equal(collection.Type, response.Type);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Delete_WithDefaultUserCollection_ReturnsBadRequest(
|
||||
Collection collection, SutProvider<CollectionsController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
collection.Type = CollectionType.DefaultUserCollection;
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationId.Returns(collection.OrganizationId);
|
||||
sutProvider.GetDependency<ICollectionRepository>()
|
||||
.GetByIdAsync(collection.Id)
|
||||
.Returns(collection);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Delete(collection.Id);
|
||||
|
||||
// Assert
|
||||
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
|
||||
var errorResponse = Assert.IsType<ErrorResponseModel>(badRequestResult.Value);
|
||||
Assert.Contains("You cannot delete a collection with the type as DefaultUserCollection", errorResponse.Message);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.DeleteAsync(Arg.Any<Collection>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Delete_WithSharedCollection_ReturnsOk(
|
||||
Collection collection, SutProvider<CollectionsController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
collection.Type = CollectionType.SharedCollection;
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationId.Returns(collection.OrganizationId);
|
||||
sutProvider.GetDependency<ICollectionRepository>()
|
||||
.GetByIdAsync(collection.Id)
|
||||
.Returns(collection);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Delete(collection.Id);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<OkResult>(result);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.Received(1)
|
||||
.DeleteAsync(collection);
|
||||
}
|
||||
}
|
@ -296,10 +296,29 @@ public class CollectionRepositoryTests
|
||||
}
|
||||
}, null);
|
||||
|
||||
// Create a default user collection (should be excluded from admin console results)
|
||||
var defaultCollection = new Collection
|
||||
{
|
||||
Name = "My Items Collection",
|
||||
OrganizationId = organization.Id,
|
||||
Type = CollectionType.DefaultUserCollection
|
||||
};
|
||||
|
||||
await collectionRepository.CreateAsync(defaultCollection, null, users: new[]
|
||||
{
|
||||
new CollectionAccessSelection()
|
||||
{
|
||||
Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true
|
||||
}
|
||||
});
|
||||
|
||||
var collections = await collectionRepository.GetManyByOrganizationIdWithPermissionsAsync(organization.Id, user.Id, true);
|
||||
|
||||
Assert.NotNull(collections);
|
||||
|
||||
// Should return only 3 collections (excluding the default user collection)
|
||||
Assert.Equal(3, collections.Count);
|
||||
|
||||
collections = collections.OrderBy(c => c.Name).ToList();
|
||||
|
||||
Assert.Collection(collections, c1 =>
|
||||
@ -463,4 +482,69 @@ public class CollectionRepositoryTests
|
||||
Assert.False(c3.Unmanaged);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test to ensure collections are properly retrieved by organization
|
||||
/// </summary>
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task GetManyByOrganizationIdAsync_Success(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ICollectionRepository collectionRepository,
|
||||
IOrganizationUserRepository organizationUserRepository)
|
||||
{
|
||||
var user = await userRepository.CreateAsync(new User
|
||||
{
|
||||
Name = "Test User",
|
||||
Email = $"test+{Guid.NewGuid()}@email.com",
|
||||
ApiKey = "TEST",
|
||||
SecurityStamp = "stamp",
|
||||
});
|
||||
|
||||
var organization = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Org",
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
Plan = "Test Plan",
|
||||
BillingEmail = "billing@email.com"
|
||||
});
|
||||
|
||||
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
});
|
||||
|
||||
var collection1 = new Collection { Name = "Collection 1", OrganizationId = organization.Id, };
|
||||
await collectionRepository.CreateAsync(collection1, null, null);
|
||||
|
||||
var collection2 = new Collection { Name = "Collection 2", OrganizationId = organization.Id, };
|
||||
await collectionRepository.CreateAsync(collection2, null, null);
|
||||
|
||||
var collection3 = new Collection { Name = "Collection 3", OrganizationId = organization.Id, };
|
||||
await collectionRepository.CreateAsync(collection3, null, null);
|
||||
|
||||
// Create a default user collection (should not be returned by this method)
|
||||
var defaultCollection = new Collection
|
||||
{
|
||||
Name = "My Items",
|
||||
OrganizationId = organization.Id,
|
||||
Type = CollectionType.DefaultUserCollection
|
||||
};
|
||||
await collectionRepository.CreateAsync(defaultCollection, null, null);
|
||||
|
||||
var collections = await collectionRepository.GetManyByOrganizationIdAsync(organization.Id);
|
||||
|
||||
Assert.NotNull(collections);
|
||||
Assert.Equal(3, collections.Count); // Should only return the 3 shared collections, excluding the default user collection
|
||||
Assert.All(collections, c => Assert.Equal(organization.Id, c.OrganizationId));
|
||||
Assert.All(collections, c => Assert.NotEqual(CollectionType.DefaultUserCollection, c.Type));
|
||||
|
||||
// Verify specific collections are returned
|
||||
Assert.Contains(collections, c => c.Name == "Collection 1");
|
||||
Assert.Contains(collections, c => c.Name == "Collection 2");
|
||||
Assert.Contains(collections, c => c.Name == "Collection 3");
|
||||
Assert.DoesNotContain(collections, c => c.Name == "My Items");
|
||||
}
|
||||
}
|
||||
|
@ -184,6 +184,87 @@ public class OrganizationUserRepositoryTests
|
||||
r.EncryptedPrivateKey == "privatekey");
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task GetManyDetailsByOrganizationAsync_WithIncludeCollections_ExcludesDefaultCollections(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
var user = await userRepository.CreateAsync(new User
|
||||
{
|
||||
Name = "Test User",
|
||||
Email = $"test+{Guid.NewGuid()}@example.com",
|
||||
ApiKey = "TEST",
|
||||
SecurityStamp = "stamp",
|
||||
});
|
||||
|
||||
var organization = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Org",
|
||||
BillingEmail = user.Email,
|
||||
Plan = "Test",
|
||||
});
|
||||
|
||||
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
});
|
||||
|
||||
// Create a regular collection
|
||||
var regularCollection = await collectionRepository.CreateAsync(new Collection
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Name = "Regular Collection",
|
||||
Type = CollectionType.SharedCollection
|
||||
});
|
||||
|
||||
// Create a default user collection
|
||||
var defaultCollection = await collectionRepository.CreateAsync(new Collection
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Name = "Default Collection",
|
||||
Type = CollectionType.DefaultUserCollection,
|
||||
DefaultUserCollectionEmail = user.Email
|
||||
});
|
||||
|
||||
// Assign the organization user to both collections
|
||||
await organizationUserRepository.ReplaceAsync(orgUser, new List<CollectionAccessSelection>
|
||||
{
|
||||
new CollectionAccessSelection
|
||||
{
|
||||
Id = regularCollection.Id,
|
||||
ReadOnly = false,
|
||||
HidePasswords = false,
|
||||
Manage = true
|
||||
},
|
||||
new CollectionAccessSelection
|
||||
{
|
||||
Id = defaultCollection.Id,
|
||||
ReadOnly = false,
|
||||
HidePasswords = false,
|
||||
Manage = true
|
||||
}
|
||||
});
|
||||
|
||||
// Get organization users with collections included
|
||||
var organizationUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync(
|
||||
organization.Id, includeGroups: false, includeCollections: true);
|
||||
|
||||
Assert.NotNull(organizationUsers);
|
||||
Assert.Single(organizationUsers);
|
||||
|
||||
var orgUserWithCollections = organizationUsers.First();
|
||||
Assert.NotNull(orgUserWithCollections.Collections);
|
||||
|
||||
// Should only include the regular collection, not the default collection
|
||||
Assert.Single(orgUserWithCollections.Collections);
|
||||
Assert.Equal(regularCollection.Id, orgUserWithCollections.Collections.First().Id);
|
||||
Assert.DoesNotContain(orgUserWithCollections.Collections, c => c.Id == defaultCollection.Id);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task GetManyDetailsByUserAsync_Works(IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -498,6 +579,17 @@ public class OrganizationUserRepositoryTests
|
||||
RevisionDate = requestTime
|
||||
});
|
||||
|
||||
// Create a default user collection that should be excluded from admin results
|
||||
var defaultCollection = await collectionRepository.CreateAsync(new Collection
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb(),
|
||||
OrganizationId = organization.Id,
|
||||
Name = "My Items",
|
||||
Type = CollectionType.DefaultUserCollection,
|
||||
CreationDate = requestTime,
|
||||
RevisionDate = requestTime
|
||||
});
|
||||
|
||||
var group1 = await groupRepository.CreateAsync(new Group
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb(),
|
||||
@ -544,6 +636,13 @@ public class OrganizationUserRepositoryTests
|
||||
ReadOnly = true,
|
||||
HidePasswords = false,
|
||||
Manage = false
|
||||
},
|
||||
new CollectionAccessSelection
|
||||
{
|
||||
Id = defaultCollection.Id,
|
||||
ReadOnly = false,
|
||||
HidePasswords = false,
|
||||
Manage = true
|
||||
}
|
||||
],
|
||||
Groups = [group1.Id]
|
||||
@ -605,7 +704,11 @@ public class OrganizationUserRepositoryTests
|
||||
var orgUser1 = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUserCollection[0].OrganizationUser.Id);
|
||||
var group1Database = await groupRepository.GetManyIdsByUserIdAsync(orgUserCollection[0].OrganizationUser.Id);
|
||||
Assert.Equal(orgUserCollection[0].OrganizationUser.Id, orgUser1.OrganizationUser.Id);
|
||||
|
||||
// Should only return the regular collection, not the default collection (even though both were assigned)
|
||||
Assert.Single(orgUser1.Collections);
|
||||
Assert.Equal(collection1.Id, orgUser1.Collections.First().Id);
|
||||
Assert.DoesNotContain(orgUser1.Collections, c => c.Id == defaultCollection.Id);
|
||||
Assert.Equal(group1.Id, group1Database.First());
|
||||
|
||||
|
||||
|
@ -0,0 +1,149 @@
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadByOrganizationIdWithPermissions]
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@IncludeAccessRelationships BIT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
C.*,
|
||||
MIN(CASE
|
||||
WHEN
|
||||
COALESCE(CU.[ReadOnly], CG.[ReadOnly], 0) = 0
|
||||
THEN 0
|
||||
ELSE 1
|
||||
END) AS [ReadOnly],
|
||||
MIN(CASE
|
||||
WHEN
|
||||
COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) = 0
|
||||
THEN 0
|
||||
ELSE 1
|
||||
END) AS [HidePasswords],
|
||||
MAX(CASE
|
||||
WHEN
|
||||
COALESCE(CU.[Manage], CG.[Manage], 0) = 0
|
||||
THEN 0
|
||||
ELSE 1
|
||||
END) AS [Manage],
|
||||
MAX(CASE
|
||||
WHEN
|
||||
CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL
|
||||
THEN 0
|
||||
ELSE 1
|
||||
END) AS [Assigned],
|
||||
CASE
|
||||
WHEN
|
||||
-- No user or group has manage rights
|
||||
NOT EXISTS(
|
||||
SELECT 1
|
||||
FROM [dbo].[CollectionUser] CU2
|
||||
JOIN [dbo].[OrganizationUser] OU2 ON CU2.[OrganizationUserId] = OU2.[Id]
|
||||
WHERE
|
||||
CU2.[CollectionId] = C.[Id] AND
|
||||
CU2.[Manage] = 1
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM [dbo].[CollectionGroup] CG2
|
||||
WHERE
|
||||
CG2.[CollectionId] = C.[Id] AND
|
||||
CG2.[Manage] = 1
|
||||
)
|
||||
THEN 1
|
||||
ELSE 0
|
||||
END AS [Unmanaged]
|
||||
FROM
|
||||
[dbo].[CollectionView] C
|
||||
LEFT JOIN
|
||||
[dbo].[OrganizationUser] OU ON C.[OrganizationId] = OU.[OrganizationId] AND OU.[UserId] = @UserId
|
||||
LEFT JOIN
|
||||
[dbo].[CollectionUser] CU ON CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = [OU].[Id]
|
||||
LEFT JOIN
|
||||
[dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]
|
||||
LEFT JOIN
|
||||
[dbo].[Group] G ON G.[Id] = GU.[GroupId]
|
||||
LEFT JOIN
|
||||
[dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId]
|
||||
WHERE
|
||||
C.[OrganizationId] = @OrganizationId AND
|
||||
C.[Type] != 1 -- Exclude DefaultUserCollection
|
||||
GROUP BY
|
||||
C.[Id],
|
||||
C.[OrganizationId],
|
||||
C.[Name],
|
||||
C.[CreationDate],
|
||||
C.[RevisionDate],
|
||||
C.[ExternalId],
|
||||
C.[DefaultUserCollectionEmail],
|
||||
C.[Type]
|
||||
|
||||
IF (@IncludeAccessRelationships = 1)
|
||||
BEGIN
|
||||
EXEC [dbo].[CollectionGroup_ReadByOrganizationId] @OrganizationId
|
||||
EXEC [dbo].[CollectionUser_ReadByOrganizationId] @OrganizationId
|
||||
END
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadByOrganizationId]
|
||||
@OrganizationId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[CollectionView]
|
||||
WHERE
|
||||
[OrganizationId] = @OrganizationId AND
|
||||
[Type] != 1 -- Exclude DefaultUserCollection
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[OrganizationUserUserDetails_ReadWithCollectionsById]
|
||||
@Id UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
EXEC [OrganizationUserUserDetails_ReadById] @Id
|
||||
|
||||
SELECT
|
||||
CU.[CollectionId] Id,
|
||||
CU.[ReadOnly],
|
||||
CU.[HidePasswords],
|
||||
CU.[Manage]
|
||||
FROM
|
||||
[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
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[CollectionUser_ReadByOrganizationUserIds]
|
||||
@OrganizationUserIds [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
CU.*
|
||||
FROM
|
||||
[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
|
||||
GO
|
Reference in New Issue
Block a user