mirror of
https://github.com/bitwarden/server
synced 2026-02-11 14:03:24 +00:00
[PM-25106] Refactor Misleading Stored Procedure/Repository Language (#6890)
* Begin migration to appropriately named sprocs * Update method and parameter names * Remove incorrect change * Changes EF to match collection type comparison * Adds integration test verifying excluded collections * Changes EF to match collection type comparison * Fix whitespacing * Fix dedented if
This commit is contained in:
@@ -155,7 +155,7 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task<OrganizationUserDetailsResponseModel> Get(Guid orgId, Guid id, bool includeGroups = false)
|
||||
{
|
||||
var (organizationUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id);
|
||||
var (organizationUser, collections) = await _organizationUserRepository.GetDetailsByIdWithSharedCollectionsAsync(id);
|
||||
if (organizationUser == null || organizationUser.OrganizationId != orgId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
|
||||
@@ -80,7 +80,7 @@ public class MembersController : Controller
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> Get(Guid id)
|
||||
{
|
||||
var (orgUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id);
|
||||
var (orgUser, collections) = await _organizationUserRepository.GetDetailsByIdWithSharedCollectionsAsync(id);
|
||||
if (orgUser == null || orgUser.OrganizationId != _currentContext.OrganizationId)
|
||||
{
|
||||
return new NotFoundResult();
|
||||
@@ -123,7 +123,7 @@ public class MembersController : Controller
|
||||
[ProducesResponseType(typeof(ListResponseModel<MemberResponseModel>), (int)HttpStatusCode.OK)]
|
||||
public async Task<IActionResult> List()
|
||||
{
|
||||
var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_currentContext.OrganizationId!.Value, includeCollections: true);
|
||||
var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_currentContext.OrganizationId!.Value, includeSharedCollections: true);
|
||||
|
||||
var orgUsersTwoFactorIsEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUserUserDetails);
|
||||
var memberResponses = organizationUserUserDetails.Select(u =>
|
||||
|
||||
@@ -81,7 +81,7 @@ public class CollectionsController : Controller
|
||||
[HttpGet("details")]
|
||||
public async Task<ListResponseModel<CollectionAccessDetailsResponseModel>> GetManyWithDetails(Guid orgId)
|
||||
{
|
||||
var allOrgCollections = await _collectionRepository.GetManyByOrganizationIdWithPermissionsAsync(
|
||||
var allOrgCollections = await _collectionRepository.GetManySharedByOrganizationIdWithPermissionsAsync(
|
||||
orgId, _currentContext.UserId.Value, true);
|
||||
|
||||
var readAllAuthorized =
|
||||
|
||||
@@ -28,21 +28,21 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
||||
/// </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);
|
||||
Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithSharedCollectionsAsync(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>
|
||||
/// <param name="includeSharedCollections">Whether to include shared collections</param>
|
||||
/// <returns>A list of OrganizationUserUserDetails</returns>
|
||||
Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups = false, bool includeCollections = false);
|
||||
Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups = false, bool includeSharedCollections = false);
|
||||
/// <inheritdoc cref="GetManyDetailsByOrganizationAsync"/>
|
||||
/// <remarks>
|
||||
/// This method is optimized for performance.
|
||||
/// Reduces database round trips by fetching all data in fewer queries.
|
||||
/// </remarks>
|
||||
Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync_vNext(Guid organizationId, bool includeGroups = false, bool includeCollections = false);
|
||||
Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync_vNext(Guid organizationId, bool includeGroups = false, bool includeSharedCollections = false);
|
||||
Task<ICollection<OrganizationUserOrganizationDetails>> GetManyDetailsByUserAsync(Guid userId,
|
||||
OrganizationUserStatusType? status = null);
|
||||
Task<OrganizationUserOrganizationDetails?> GetDetailsByUserAsync(Guid userId, Guid organizationId,
|
||||
|
||||
@@ -45,7 +45,7 @@ public interface ICollectionRepository : IRepository<Collection, Guid>
|
||||
/// 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);
|
||||
Task<ICollection<CollectionAdminDetails>> GetManySharedByOrganizationIdWithPermissionsAsync(Guid organizationId, Guid userId, bool includeAccessRelationships);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the collection by Id, including permission info for the specified user.
|
||||
|
||||
@@ -187,12 +187,12 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
return results.SingleOrDefault();
|
||||
}
|
||||
}
|
||||
public async Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithCollectionsAsync(Guid id)
|
||||
public async Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithSharedCollectionsAsync(Guid id)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryMultipleAsync(
|
||||
"[dbo].[OrganizationUserUserDetails_ReadWithCollectionsById]",
|
||||
"[dbo].[OrganizationUserUserDetails_ReadWithSharedCollectionsById]",
|
||||
new { Id = id },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
@@ -202,7 +202,7 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups, bool includeCollections)
|
||||
public async Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups, bool includeSharedCollections)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
@@ -216,7 +216,7 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
|
||||
var users = results.ToList();
|
||||
|
||||
if (!includeCollections && !includeGroups)
|
||||
if (!includeSharedCollections && !includeGroups)
|
||||
{
|
||||
return users;
|
||||
}
|
||||
@@ -231,10 +231,10 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
commandType: CommandType.StoredProcedure)).GroupBy(u => u.OrganizationUserId).ToList();
|
||||
}
|
||||
|
||||
if (includeCollections)
|
||||
if (includeSharedCollections)
|
||||
{
|
||||
userCollections = (await connection.QueryAsync<CollectionUser>(
|
||||
"[dbo].[CollectionUser_ReadByOrganizationUserIds]",
|
||||
"[dbo].[CollectionUser_ReadSharedCollectionsByOrganizationUserIds]",
|
||||
new { OrganizationUserIds = orgUserIds },
|
||||
commandType: CommandType.StoredProcedure)).GroupBy(u => u.OrganizationUserId).ToList();
|
||||
}
|
||||
@@ -267,7 +267,7 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync_vNext(Guid organizationId, bool includeGroups, bool includeCollections)
|
||||
public async Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync_vNext(Guid organizationId, bool includeGroups, bool includeSharedCollections)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
@@ -278,7 +278,7 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
IncludeGroups = includeGroups,
|
||||
IncludeCollections = includeCollections
|
||||
IncludeCollections = includeSharedCollections
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
@@ -297,7 +297,7 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
|
||||
// Read collection associations (third result set, if requested)
|
||||
Dictionary<Guid, List<CollectionAccessSelection>>? userCollectionMap = null;
|
||||
if (includeCollections)
|
||||
if (includeSharedCollections)
|
||||
{
|
||||
var collectionUsers = await results.ReadAsync<CollectionUser>();
|
||||
userCollectionMap = collectionUsers
|
||||
|
||||
@@ -153,12 +153,12 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<CollectionAdminDetails>> GetManyByOrganizationIdWithPermissionsAsync(Guid organizationId, Guid userId, bool includeAccessRelationships)
|
||||
public async Task<ICollection<CollectionAdminDetails>> GetManySharedByOrganizationIdWithPermissionsAsync(Guid organizationId, Guid userId, bool includeAccessRelationships)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryMultipleAsync(
|
||||
$"[{Schema}].[Collection_ReadByOrganizationIdWithPermissions]",
|
||||
$"[{Schema}].[Collection_ReadSharedCollectionsByOrganizationIdWithPermissions]",
|
||||
new { OrganizationId = organizationId, UserId = userId, IncludeAccessRelationships = includeAccessRelationships },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
|
||||
@@ -350,7 +350,7 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
public async Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithCollectionsAsync(Guid id)
|
||||
public async Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithSharedCollectionsAsync(Guid id)
|
||||
{
|
||||
var organizationUserUserDetails = await GetDetailsByIdAsync(id);
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
@@ -359,7 +359,7 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
var query = from ou in dbContext.OrganizationUsers
|
||||
join cu in dbContext.CollectionUsers on ou.Id equals cu.OrganizationUserId
|
||||
join c in dbContext.Collections on cu.CollectionId equals c.Id
|
||||
where ou.Id == id && c.Type != CollectionType.DefaultUserCollection
|
||||
where ou.Id == id && c.Type == CollectionType.SharedCollection
|
||||
select cu;
|
||||
var collections = await query.Select(cu => new CollectionAccessSelection
|
||||
{
|
||||
@@ -438,7 +438,7 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups, bool includeCollections)
|
||||
public async Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups, bool includeSharedCollections)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
@@ -448,7 +448,7 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
where ou.OrganizationId == organizationId
|
||||
select ou).ToListAsync();
|
||||
|
||||
if (!includeCollections && !includeGroups)
|
||||
if (!includeSharedCollections && !includeGroups)
|
||||
{
|
||||
return users;
|
||||
}
|
||||
@@ -467,12 +467,12 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
.GroupBy(g => g.OrganizationUserId).ToList();
|
||||
}
|
||||
|
||||
if (includeCollections)
|
||||
if (includeSharedCollections)
|
||||
{
|
||||
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
|
||||
where c.Type == CollectionType.SharedCollection
|
||||
select cu).ToListAsync())
|
||||
.GroupBy(c => c.OrganizationUserId).ToList();
|
||||
}
|
||||
@@ -506,7 +506,7 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
}
|
||||
|
||||
public async Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync_vNext(
|
||||
Guid organizationId, bool includeGroups, bool includeCollections)
|
||||
Guid organizationId, bool includeGroups, bool includeSharedCollections)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
@@ -541,7 +541,7 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
? ou.GroupUsers.Select(gu => gu.GroupId).ToList()
|
||||
: new List<Guid>(),
|
||||
|
||||
Collections = includeCollections
|
||||
Collections = includeSharedCollections
|
||||
? ou.CollectionUsers
|
||||
.Where(cu => cu.Collection.Type == CollectionType.SharedCollection)
|
||||
.Select(cu => new CollectionAccessSelection
|
||||
|
||||
@@ -303,7 +303,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<CollectionAdminDetails>> GetManyByOrganizationIdWithPermissionsAsync(
|
||||
public async Task<ICollection<CollectionAdminDetails>> GetManySharedByOrganizationIdWithPermissionsAsync(
|
||||
Guid organizationId, Guid userId, bool includeAccessRelationships)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
|
||||
@@ -62,7 +62,7 @@ public class CollectionAdminDetailsQuery : IQuery<CollectionAdminDetails>
|
||||
{
|
||||
baseCollectionQuery = baseCollectionQuery.Where(x =>
|
||||
x.c.OrganizationId == _organizationId &&
|
||||
x.c.Type != CollectionType.DefaultUserCollection);
|
||||
x.c.Type == CollectionType.SharedCollection);
|
||||
}
|
||||
else if (_collectionId.HasValue)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
CREATE PROCEDURE [dbo].[CollectionUser_ReadSharedCollectionsByOrganizationUserIds]
|
||||
@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] = 0 -- Only SharedCollection
|
||||
END
|
||||
@@ -0,0 +1,23 @@
|
||||
CREATE PROCEDURE [dbo].[OrganizationUserUserDetails_ReadWithSharedCollectionsById]
|
||||
@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] = 0 -- Only SharedCollection
|
||||
END
|
||||
@@ -0,0 +1,86 @@
|
||||
CREATE PROCEDURE [dbo].[Collection_ReadSharedCollectionsByOrganizationIdWithPermissions]
|
||||
@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] = 0 -- Only SharedCollection
|
||||
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
|
||||
@@ -287,7 +287,7 @@ public class OrganizationUsersControllerTests
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetDetailsByIdWithCollectionsAsync(organizationUser.Id)
|
||||
.GetDetailsByIdWithSharedCollectionsAsync(organizationUser.Id)
|
||||
.Returns((organizationUser, collections));
|
||||
|
||||
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
|
||||
|
||||
@@ -107,7 +107,7 @@ public class CollectionsControllerTests
|
||||
|
||||
await sutProvider.Sut.GetManyWithDetails(organization.Id);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdWithPermissionsAsync(organization.Id, userId, true);
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManySharedByOrganizationIdWithPermissionsAsync(organization.Id, userId, true);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -143,12 +143,12 @@ public class CollectionsControllerTests
|
||||
.Returns(AuthorizationResult.Success());
|
||||
|
||||
sutProvider.GetDependency<ICollectionRepository>()
|
||||
.GetManyByOrganizationIdWithPermissionsAsync(organization.Id, userId, true)
|
||||
.GetManySharedByOrganizationIdWithPermissionsAsync(organization.Id, userId, true)
|
||||
.Returns(collections);
|
||||
|
||||
var response = await sutProvider.Sut.GetManyWithDetails(organization.Id);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdWithPermissionsAsync(organization.Id, userId, true);
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManySharedByOrganizationIdWithPermissionsAsync(organization.Id, userId, true);
|
||||
Assert.Single(response.Data);
|
||||
Assert.All(response.Data, c => Assert.Equal(organization.Id, c.OrganizationId));
|
||||
Assert.All(response.Data, c => Assert.Equal(managedCollection.Id, c.Id));
|
||||
|
||||
@@ -312,7 +312,7 @@ public class CollectionRepositoryTests
|
||||
}
|
||||
});
|
||||
|
||||
var collections = await collectionRepository.GetManyByOrganizationIdWithPermissionsAsync(organization.Id, user.Id, true);
|
||||
var collections = await collectionRepository.GetManySharedByOrganizationIdWithPermissionsAsync(organization.Id, user.Id, true);
|
||||
|
||||
Assert.NotNull(collections);
|
||||
|
||||
@@ -442,7 +442,7 @@ public class CollectionRepositoryTests
|
||||
}
|
||||
}, null);
|
||||
|
||||
var collections = await collectionRepository.GetManyByOrganizationIdWithPermissionsAsync(organization.Id, user.Id, true);
|
||||
var collections = await collectionRepository.GetManySharedByOrganizationIdWithPermissionsAsync(organization.Id, user.Id, true);
|
||||
|
||||
Assert.NotNull(collections);
|
||||
|
||||
|
||||
@@ -430,7 +430,7 @@ public class OrganizationUserRepositoryTests
|
||||
|
||||
// Get organization users with collections included
|
||||
var organizationUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync(
|
||||
organization.Id, includeGroups: false, includeCollections: true);
|
||||
organization.Id, includeGroups: false, includeSharedCollections: true);
|
||||
|
||||
Assert.NotNull(organizationUsers);
|
||||
Assert.Single(organizationUsers);
|
||||
@@ -444,6 +444,83 @@ public class OrganizationUserRepositoryTests
|
||||
Assert.DoesNotContain(orgUserWithCollections.Collections, c => c.Id == defaultCollection.Id);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task GetDetailsByIdWithSharedCollectionsAsync_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 shared collection
|
||||
var sharedCollection = await collectionRepository.CreateAsync(new Collection
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Name = "Shared 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 = sharedCollection.Id,
|
||||
ReadOnly = false,
|
||||
HidePasswords = false,
|
||||
Manage = true
|
||||
},
|
||||
new CollectionAccessSelection
|
||||
{
|
||||
Id = defaultCollection.Id,
|
||||
ReadOnly = false,
|
||||
HidePasswords = false,
|
||||
Manage = true
|
||||
}
|
||||
});
|
||||
|
||||
// Get organization user details with collections
|
||||
var (orgUserDetails, collections) = await organizationUserRepository.GetDetailsByIdWithSharedCollectionsAsync(orgUser.Id);
|
||||
|
||||
Assert.NotNull(orgUserDetails);
|
||||
Assert.NotNull(collections);
|
||||
|
||||
// Should only include the shared collection, not the default collection
|
||||
Assert.Single(collections);
|
||||
Assert.Equal(sharedCollection.Id, collections.First().Id);
|
||||
Assert.DoesNotContain(collections, c => c.Id == defaultCollection.Id);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task GetManyDetailsByUserAsync_Works(IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
@@ -835,7 +912,7 @@ public class OrganizationUserRepositoryTests
|
||||
|
||||
await organizationUserRepository.CreateManyAsync(orgUserCollection);
|
||||
|
||||
var orgUser1 = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUserCollection[0].OrganizationUser.Id);
|
||||
var orgUser1 = await organizationUserRepository.GetDetailsByIdWithSharedCollectionsAsync(orgUserCollection[0].OrganizationUser.Id);
|
||||
var group1Database = await groupRepository.GetManyIdsByUserIdAsync(orgUserCollection[0].OrganizationUser.Id);
|
||||
Assert.Equal(orgUserCollection[0].OrganizationUser.Id, orgUser1.OrganizationUser.Id);
|
||||
|
||||
@@ -846,13 +923,13 @@ public class OrganizationUserRepositoryTests
|
||||
Assert.Equal(group1.Id, group1Database.First());
|
||||
|
||||
|
||||
var orgUser2 = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUserCollection[1].OrganizationUser.Id);
|
||||
var orgUser2 = await organizationUserRepository.GetDetailsByIdWithSharedCollectionsAsync(orgUserCollection[1].OrganizationUser.Id);
|
||||
var group2Database = await groupRepository.GetManyIdsByUserIdAsync(orgUserCollection[1].OrganizationUser.Id);
|
||||
Assert.Equal(orgUserCollection[1].OrganizationUser.Id, orgUser2.OrganizationUser.Id);
|
||||
Assert.Equal(collection2.Id, orgUser2.Collections.First().Id);
|
||||
Assert.Equal(group2.Id, group2Database.First());
|
||||
|
||||
var orgUser3 = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUserCollection[2].OrganizationUser.Id);
|
||||
var orgUser3 = await organizationUserRepository.GetDetailsByIdWithSharedCollectionsAsync(orgUserCollection[2].OrganizationUser.Id);
|
||||
var group3Database = await groupRepository.GetManyIdsByUserIdAsync(orgUserCollection[2].OrganizationUser.Id);
|
||||
Assert.Equal(orgUserCollection[2].OrganizationUser.Id, orgUser3.OrganizationUser.Id);
|
||||
Assert.Equal(collection3.Id, orgUser3.Collections.First().Id);
|
||||
@@ -928,7 +1005,7 @@ public class OrganizationUserRepositoryTests
|
||||
AccessSecretsManager = true
|
||||
});
|
||||
|
||||
var responseModel = await organizationUserRepository.GetManyDetailsByOrganizationAsync_vNext(organization.Id, includeGroups: false, includeCollections: false);
|
||||
var responseModel = await organizationUserRepository.GetManyDetailsByOrganizationAsync_vNext(organization.Id, includeGroups: false, includeSharedCollections: false);
|
||||
|
||||
Assert.NotNull(responseModel);
|
||||
Assert.Equal(2, responseModel.Count);
|
||||
@@ -1083,7 +1160,7 @@ public class OrganizationUserRepositoryTests
|
||||
|
||||
await organizationUserRepository.CreateManyAsync(createOrgUserWithCollections);
|
||||
|
||||
var responseModel = await organizationUserRepository.GetManyDetailsByOrganizationAsync_vNext(organization.Id, includeGroups: true, includeCollections: true);
|
||||
var responseModel = await organizationUserRepository.GetManyDetailsByOrganizationAsync_vNext(organization.Id, includeGroups: true, includeSharedCollections: true);
|
||||
|
||||
Assert.NotNull(responseModel);
|
||||
Assert.Single(responseModel);
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
-- Add stored procedures for reading shared collections (Type = 0)
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[CollectionUser_ReadSharedCollectionsByOrganizationUserIds]
|
||||
@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] = 0 -- Only SharedCollection
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[OrganizationUserUserDetails_ReadWithSharedCollectionsById]
|
||||
@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] = 0 -- Only SharedCollection
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadSharedCollectionsByOrganizationIdWithPermissions]
|
||||
@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] = 0 -- Only SharedCollection
|
||||
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
|
||||
Reference in New Issue
Block a user