mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[AC-2084] Include Collection permissions for admin endpoints (#3793)
* [AC-2084] Add documentation to existing collection repository getters * [AC-2084] Add new CollectionAdminDetails model * [AC-2084] Add SQL and migration scripts * [AC-2084] Introduce new repository methods to include permission details for collections * [AC-2084] Add EF repository methods and integration tests * [AC-2084] Update CollectionsController and response models * [AC-2084] Fix failing SqlServer test * [AC-2084] Clean up admin endpoint response models - vNext endpoints should now always return CollectionDetailsResponse models - Update constructors in CollectionDetailsResponseModel to be more explicit and add named static constructors for additional clarity * [AC-2084] Fix failing tests * [AC-2084] Fix potential provider/member bug * [AC-2084] Fix broken collections controller * [AC-2084] Cleanup collection response model types and constructors * [AC-2084] Remove redundant authorization check * [AC-2084] Cleanup ambiguous model name * [AC-2084] Add GroupBy clause to sprocs * [AC-2084] Add GroupBy logic to EF repository * [AC-2084] Update collection repository tests * [AC-2084] Update migration script date * Update migration script date --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: kejaeger <138028972+kejaeger@users.noreply.github.com>
This commit is contained in:
parent
25c87214ff
commit
d965166a37
@ -553,47 +553,38 @@ public class CollectionsController : Controller
|
|||||||
private async Task<CollectionAccessDetailsResponseModel> GetDetails_vNext(Guid id)
|
private async Task<CollectionAccessDetailsResponseModel> GetDetails_vNext(Guid id)
|
||||||
{
|
{
|
||||||
// New flexible collections logic
|
// New flexible collections logic
|
||||||
var (collection, access) = await _collectionRepository.GetByIdWithAccessAsync(id);
|
var collectionAdminDetails =
|
||||||
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ReadWithAccess)).Succeeded;
|
await _collectionRepository.GetByIdWithPermissionsAsync(id, _currentContext.UserId, true);
|
||||||
|
|
||||||
|
var authorized = (await _authorizationService.AuthorizeAsync(User, collectionAdminDetails, BulkCollectionOperations.ReadWithAccess)).Succeeded;
|
||||||
if (!authorized)
|
if (!authorized)
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CollectionAccessDetailsResponseModel(collection, access.Groups, access.Users);
|
return new CollectionAccessDetailsResponseModel(collectionAdminDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ListResponseModel<CollectionAccessDetailsResponseModel>> GetManyWithDetails_vNext(Guid orgId)
|
private async Task<ListResponseModel<CollectionAccessDetailsResponseModel>> GetManyWithDetails_vNext(Guid orgId)
|
||||||
{
|
{
|
||||||
// We always need to know which collections the current user is assigned to
|
var allOrgCollections = await _collectionRepository.GetManyByOrganizationIdWithPermissionsAsync(
|
||||||
var assignedOrgCollections = await _collectionRepository
|
orgId, _currentContext.UserId.Value, true);
|
||||||
.GetManyByUserIdWithAccessAsync(_currentContext.UserId.Value, orgId, true);
|
|
||||||
|
|
||||||
var readAllAuthorized =
|
var readAllAuthorized =
|
||||||
(await _authorizationService.AuthorizeAsync(User, CollectionOperations.ReadAllWithAccess(orgId))).Succeeded;
|
(await _authorizationService.AuthorizeAsync(User, CollectionOperations.ReadAllWithAccess(orgId))).Succeeded;
|
||||||
if (readAllAuthorized)
|
if (readAllAuthorized)
|
||||||
{
|
{
|
||||||
// The user can view all collections, but they may not always be assigned to all of them
|
return new ListResponseModel<CollectionAccessDetailsResponseModel>(
|
||||||
var allOrgCollections = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(orgId);
|
allOrgCollections.Select(c => new CollectionAccessDetailsResponseModel(c))
|
||||||
|
|
||||||
return new ListResponseModel<CollectionAccessDetailsResponseModel>(allOrgCollections.Select(c =>
|
|
||||||
new CollectionAccessDetailsResponseModel(c.Item1, c.Item2.Groups, c.Item2.Users)
|
|
||||||
{
|
|
||||||
// Manually determine which collections they're assigned to
|
|
||||||
Assigned = assignedOrgCollections.Any(ac => ac.Item1.Id == c.Item1.Id)
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter the assigned collections to only return those where the user has Manage permission
|
// Filter collections to only return those where the user has Manage permission
|
||||||
var manageableOrgCollections = assignedOrgCollections.Where(c => c.Item1.Manage).ToList();
|
var manageableOrgCollections = allOrgCollections.Where(c => c.Manage).ToList();
|
||||||
|
|
||||||
return new ListResponseModel<CollectionAccessDetailsResponseModel>(manageableOrgCollections.Select(c =>
|
return new ListResponseModel<CollectionAccessDetailsResponseModel>(manageableOrgCollections.Select(c =>
|
||||||
new CollectionAccessDetailsResponseModel(c.Item1, c.Item2.Groups, c.Item2.Users)
|
new CollectionAccessDetailsResponseModel(c)
|
||||||
{
|
));
|
||||||
Assigned = true // Mapping from manageableOrgCollections implies they're all assigned
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ListResponseModel<CollectionResponseModel>> GetByOrgId_vNext(Guid orgId)
|
private async Task<ListResponseModel<CollectionResponseModel>> GetByOrgId_vNext(Guid orgId)
|
||||||
@ -629,7 +620,7 @@ public class CollectionsController : Controller
|
|||||||
return responses;
|
return responses;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<CollectionResponseModel> Post_vNext(Guid orgId, [FromBody] CollectionRequestModel model)
|
private async Task<CollectionAccessDetailsResponseModel> Post_vNext(Guid orgId, [FromBody] CollectionRequestModel model)
|
||||||
{
|
{
|
||||||
var collection = model.ToCollection(orgId);
|
var collection = model.ToCollection(orgId);
|
||||||
|
|
||||||
@ -644,21 +635,18 @@ public class CollectionsController : Controller
|
|||||||
|
|
||||||
await _collectionService.SaveAsync(collection, groups, users);
|
await _collectionService.SaveAsync(collection, groups, users);
|
||||||
|
|
||||||
if (!_currentContext.UserId.HasValue || await _currentContext.ProviderUserForOrgAsync(orgId))
|
if (!_currentContext.UserId.HasValue || (_currentContext.GetOrganization(orgId) == null && await _currentContext.ProviderUserForOrgAsync(orgId)))
|
||||||
{
|
{
|
||||||
return new CollectionResponseModel(collection);
|
return new CollectionAccessDetailsResponseModel(collection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a user, fetch the collection to get the latest permission details
|
// If we have a user, fetch the latest collection permission details
|
||||||
var userCollectionDetails = await _collectionRepository.GetByIdAsync(collection.Id,
|
var collectionWithPermissions = await _collectionRepository.GetByIdWithPermissionsAsync(collection.Id, _currentContext.UserId.Value, false);
|
||||||
_currentContext.UserId.Value, await FlexibleCollectionsIsEnabledAsync(collection.OrganizationId));
|
|
||||||
|
|
||||||
return userCollectionDetails == null
|
return new CollectionAccessDetailsResponseModel(collectionWithPermissions);
|
||||||
? new CollectionResponseModel(collection)
|
|
||||||
: new CollectionDetailsResponseModel(userCollectionDetails);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<CollectionResponseModel> Put_vNext(Guid id, CollectionRequestModel model)
|
private async Task<CollectionAccessDetailsResponseModel> Put_vNext(Guid id, CollectionRequestModel model)
|
||||||
{
|
{
|
||||||
var collection = await _collectionRepository.GetByIdAsync(id);
|
var collection = await _collectionRepository.GetByIdAsync(id);
|
||||||
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Update)).Succeeded;
|
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Update)).Succeeded;
|
||||||
@ -671,17 +659,15 @@ public class CollectionsController : Controller
|
|||||||
var users = model.Users?.Select(g => g.ToSelectionReadOnly());
|
var users = model.Users?.Select(g => g.ToSelectionReadOnly());
|
||||||
await _collectionService.SaveAsync(model.ToCollection(collection), groups, users);
|
await _collectionService.SaveAsync(model.ToCollection(collection), groups, users);
|
||||||
|
|
||||||
if (!_currentContext.UserId.HasValue || await _currentContext.ProviderUserForOrgAsync(collection.OrganizationId))
|
if (!_currentContext.UserId.HasValue || (_currentContext.GetOrganization(collection.OrganizationId) == null && await _currentContext.ProviderUserForOrgAsync(collection.OrganizationId)))
|
||||||
{
|
{
|
||||||
return new CollectionResponseModel(collection);
|
return new CollectionAccessDetailsResponseModel(collection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a user, fetch the collection details to get the latest permission details for the user
|
// If we have a user, fetch the latest collection permission details
|
||||||
var updatedCollectionDetails = await _collectionRepository.GetByIdAsync(id, _currentContext.UserId.Value, await FlexibleCollectionsIsEnabledAsync(collection.OrganizationId));
|
var collectionWithPermissions = await _collectionRepository.GetByIdWithPermissionsAsync(collection.Id, _currentContext.UserId.Value, false);
|
||||||
|
|
||||||
return updatedCollectionDetails == null
|
return new CollectionAccessDetailsResponseModel(collectionWithPermissions);
|
||||||
? new CollectionResponseModel(collection)
|
|
||||||
: new CollectionDetailsResponseModel(updatedCollectionDetails);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PutUsers_vNext(Guid id, IEnumerable<SelectionReadOnlyRequestModel> model)
|
private async Task PutUsers_vNext(Guid id, IEnumerable<SelectionReadOnlyRequestModel> model)
|
||||||
|
@ -26,8 +26,15 @@ public class CollectionResponseModel : ResponseModel
|
|||||||
public string ExternalId { get; set; }
|
public string ExternalId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response model for a collection that is always assigned to the requesting user, including permissions.
|
||||||
|
/// </summary>
|
||||||
public class CollectionDetailsResponseModel : CollectionResponseModel
|
public class CollectionDetailsResponseModel : CollectionResponseModel
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a response model for when the user is assumed to be assigned to the collection with permissions.
|
||||||
|
/// e.g. The collection details comes from a repository method that only returns collections the user is assigned to.
|
||||||
|
/// </summary>
|
||||||
public CollectionDetailsResponseModel(CollectionDetails collectionDetails)
|
public CollectionDetailsResponseModel(CollectionDetails collectionDetails)
|
||||||
: base(collectionDetails, "collectionDetails")
|
: base(collectionDetails, "collectionDetails")
|
||||||
{
|
{
|
||||||
@ -43,6 +50,27 @@ public class CollectionDetailsResponseModel : CollectionResponseModel
|
|||||||
|
|
||||||
public class CollectionAccessDetailsResponseModel : CollectionResponseModel
|
public class CollectionAccessDetailsResponseModel : CollectionResponseModel
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a response model for when the requesting user is assumed not assigned to the collection.
|
||||||
|
/// No user permissions are included.
|
||||||
|
///
|
||||||
|
/// Ideally, the CollectionAdminDetails constructor should be used instead wherever possible. This is only
|
||||||
|
/// used in the case of MSPs where the Provider user will likely never be assigned to the collection.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collection"></param>
|
||||||
|
public CollectionAccessDetailsResponseModel(Collection collection)
|
||||||
|
: base(collection, "collectionAccessDetails")
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a response model for when the requesting user is assumed not assigned to the collection. Includes
|
||||||
|
/// the other groups and user relationships for the collection.
|
||||||
|
/// No user permissions are included.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collection"></param>
|
||||||
|
/// <param name="groups"></param>
|
||||||
|
/// <param name="users"></param>
|
||||||
|
[Obsolete("Use the CollectionAdminDetails constructor instead.")]
|
||||||
public CollectionAccessDetailsResponseModel(Collection collection, IEnumerable<CollectionAccessSelection> groups, IEnumerable<CollectionAccessSelection> users)
|
public CollectionAccessDetailsResponseModel(Collection collection, IEnumerable<CollectionAccessSelection> groups, IEnumerable<CollectionAccessSelection> users)
|
||||||
: base(collection, "collectionAccessDetails")
|
: base(collection, "collectionAccessDetails")
|
||||||
{
|
{
|
||||||
@ -50,6 +78,21 @@ public class CollectionAccessDetailsResponseModel : CollectionResponseModel
|
|||||||
Users = users.Select(g => new SelectionReadOnlyResponseModel(g));
|
Users = users.Select(g => new SelectionReadOnlyResponseModel(g));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a response model for when the requesting user's assignment is available via CollectionAdminDetails.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collection"></param>
|
||||||
|
public CollectionAccessDetailsResponseModel(CollectionAdminDetails collection)
|
||||||
|
: base(collection, "collectionAccessDetails")
|
||||||
|
{
|
||||||
|
Assigned = collection.Assigned;
|
||||||
|
ReadOnly = collection.ReadOnly;
|
||||||
|
HidePasswords = collection.HidePasswords;
|
||||||
|
Manage = collection.Manage;
|
||||||
|
Groups = collection.Groups?.Select(g => new SelectionReadOnlyResponseModel(g)) ?? Enumerable.Empty<SelectionReadOnlyResponseModel>();
|
||||||
|
Users = collection.Users?.Select(g => new SelectionReadOnlyResponseModel(g)) ?? Enumerable.Empty<SelectionReadOnlyResponseModel>();
|
||||||
|
}
|
||||||
|
|
||||||
public IEnumerable<SelectionReadOnlyResponseModel> Groups { get; set; }
|
public IEnumerable<SelectionReadOnlyResponseModel> Groups { get; set; }
|
||||||
public IEnumerable<SelectionReadOnlyResponseModel> Users { get; set; }
|
public IEnumerable<SelectionReadOnlyResponseModel> Users { get; set; }
|
||||||
|
|
||||||
@ -57,4 +100,8 @@ public class CollectionAccessDetailsResponseModel : CollectionResponseModel
|
|||||||
/// True if the acting user is explicitly assigned to the collection
|
/// True if the acting user is explicitly assigned to the collection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Assigned { get; set; }
|
public bool Assigned { get; set; }
|
||||||
|
|
||||||
|
public bool ReadOnly { get; set; }
|
||||||
|
public bool HidePasswords { get; set; }
|
||||||
|
public bool Manage { get; set; }
|
||||||
}
|
}
|
||||||
|
17
src/Core/Models/Data/CollectionAdminDetails.cs
Normal file
17
src/Core/Models/Data/CollectionAdminDetails.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
#nullable enable
|
||||||
|
namespace Bit.Core.Models.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Collection information that includes permission details for a particular user along with optional
|
||||||
|
/// access relationships for Groups/Users. Used for collection management.
|
||||||
|
/// </summary>
|
||||||
|
public class CollectionAdminDetails : CollectionDetails
|
||||||
|
{
|
||||||
|
public IEnumerable<CollectionAccessSelection>? Groups { get; set; } = new List<CollectionAccessSelection>();
|
||||||
|
public IEnumerable<CollectionAccessSelection>? Users { get; set; } = new List<CollectionAccessSelection>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flag for whether the user has been explicitly assigned to the collection either directly or through a group.
|
||||||
|
/// </summary>
|
||||||
|
public bool Assigned { get; set; }
|
||||||
|
}
|
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
namespace Bit.Core.Models.Data;
|
namespace Bit.Core.Models.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Collection information that includes permission details for a particular user
|
||||||
|
/// </summary>
|
||||||
public class CollectionDetails : Collection
|
public class CollectionDetails : Collection
|
||||||
{
|
{
|
||||||
public bool ReadOnly { get; set; }
|
public bool ReadOnly { get; set; }
|
||||||
|
@ -6,14 +6,64 @@ namespace Bit.Core.Repositories;
|
|||||||
public interface ICollectionRepository : IRepository<Collection, Guid>
|
public interface ICollectionRepository : IRepository<Collection, Guid>
|
||||||
{
|
{
|
||||||
Task<int> GetCountByOrganizationIdAsync(Guid organizationId);
|
Task<int> GetCountByOrganizationIdAsync(Guid organizationId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a collection and fetches group/user associations for the collection.
|
||||||
|
/// </summary>
|
||||||
Task<Tuple<Collection, CollectionAccessDetails>> GetByIdWithAccessAsync(Guid id);
|
Task<Tuple<Collection, CollectionAccessDetails>> GetByIdWithAccessAsync(Guid id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a collection with permission details for the provided userId and fetches group/user associations for
|
||||||
|
/// the collection.
|
||||||
|
/// If the user does not have a relationship with the collection, nothing is returned.
|
||||||
|
/// </summary>
|
||||||
Task<Tuple<CollectionDetails, CollectionAccessDetails>> GetByIdWithAccessAsync(Guid id, Guid userId, bool useFlexibleCollections);
|
Task<Tuple<CollectionDetails, CollectionAccessDetails>> GetByIdWithAccessAsync(Guid id, Guid userId, bool useFlexibleCollections);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return all collections that belong to the organization. Does not include any permission details or group/user
|
||||||
|
/// access relationships.
|
||||||
|
/// </summary>
|
||||||
Task<ICollection<Collection>> GetManyByOrganizationIdAsync(Guid organizationId);
|
Task<ICollection<Collection>> GetManyByOrganizationIdAsync(Guid organizationId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return all collections that belong to the organization. Includes group/user access relationships for each collection.
|
||||||
|
/// </summary>
|
||||||
Task<ICollection<Tuple<Collection, CollectionAccessDetails>>> GetManyByOrganizationIdWithAccessAsync(Guid organizationId);
|
Task<ICollection<Tuple<Collection, CollectionAccessDetails>>> GetManyByOrganizationIdWithAccessAsync(Guid organizationId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns collections that both, belong to the organization AND have an access relationship with the provided user.
|
||||||
|
/// Includes permission details for the provided user and group/user access relationships for each collection.
|
||||||
|
/// </summary>
|
||||||
Task<ICollection<Tuple<CollectionDetails, CollectionAccessDetails>>> GetManyByUserIdWithAccessAsync(Guid userId, Guid organizationId, bool useFlexibleCollections);
|
Task<ICollection<Tuple<CollectionDetails, CollectionAccessDetails>>> GetManyByUserIdWithAccessAsync(Guid userId, Guid organizationId, bool useFlexibleCollections);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a collection with permission details for the provided userId. Does not include group/user access
|
||||||
|
/// relationships.
|
||||||
|
/// If the user does not have a relationship with the collection, nothing is returned.
|
||||||
|
/// </summary>
|
||||||
Task<CollectionDetails> GetByIdAsync(Guid id, Guid userId, bool useFlexibleCollections);
|
Task<CollectionDetails> GetByIdAsync(Guid id, Guid userId, bool useFlexibleCollections);
|
||||||
Task<ICollection<Collection>> GetManyByManyIdsAsync(IEnumerable<Guid> collectionIds);
|
Task<ICollection<Collection>> GetManyByManyIdsAsync(IEnumerable<Guid> collectionIds);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return all collections a user has access to across all of the organization they're a member of. Includes permission
|
||||||
|
/// details for each collection.
|
||||||
|
/// </summary>
|
||||||
Task<ICollection<CollectionDetails>> GetManyByUserIdAsync(Guid userId, bool useFlexibleCollections);
|
Task<ICollection<CollectionDetails>> GetManyByUserIdAsync(Guid userId, bool useFlexibleCollections);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all 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.
|
||||||
|
/// </summary>
|
||||||
|
Task<ICollection<CollectionAdminDetails>> GetManyByOrganizationIdWithPermissionsAsync(Guid organizationId, Guid userId, bool includeAccessRelationships);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the collection by Id, 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 collection.
|
||||||
|
/// </summary>
|
||||||
|
Task<CollectionAdminDetails> GetByIdWithPermissionsAsync(Guid collectionId, Guid? userId, bool includeAccessRelationships);
|
||||||
|
|
||||||
Task CreateAsync(Collection obj, IEnumerable<CollectionAccessSelection> groups, IEnumerable<CollectionAccessSelection> users);
|
Task CreateAsync(Collection obj, IEnumerable<CollectionAccessSelection> groups, IEnumerable<CollectionAccessSelection> users);
|
||||||
Task ReplaceAsync(Collection obj, IEnumerable<CollectionAccessSelection> groups, IEnumerable<CollectionAccessSelection> users);
|
Task ReplaceAsync(Collection obj, IEnumerable<CollectionAccessSelection> groups, IEnumerable<CollectionAccessSelection> users);
|
||||||
Task DeleteUserAsync(Guid collectionId, Guid organizationUserId);
|
Task DeleteUserAsync(Guid collectionId, Guid organizationUserId);
|
||||||
|
@ -225,6 +225,75 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ICollection<CollectionAdminDetails>> GetManyByOrganizationIdWithPermissionsAsync(Guid organizationId, Guid userId, bool includeAccessRelationships)
|
||||||
|
{
|
||||||
|
using (var connection = new SqlConnection(ConnectionString))
|
||||||
|
{
|
||||||
|
var results = await connection.QueryMultipleAsync(
|
||||||
|
$"[{Schema}].[Collection_ReadByOrganizationIdWithPermissions]",
|
||||||
|
new { OrganizationId = organizationId, UserId = userId, IncludeAccessRelationships = includeAccessRelationships },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
var collections = (await results.ReadAsync<CollectionAdminDetails>()).ToList();
|
||||||
|
|
||||||
|
if (!includeAccessRelationships)
|
||||||
|
{
|
||||||
|
return collections;
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups = (await results.ReadAsync<CollectionGroup>())
|
||||||
|
.GroupBy(g => g.CollectionId)
|
||||||
|
.ToList();
|
||||||
|
var users = (await results.ReadAsync<CollectionUser>())
|
||||||
|
.GroupBy(u => u.CollectionId)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var collection in collections)
|
||||||
|
{
|
||||||
|
collection.Groups = groups
|
||||||
|
.FirstOrDefault(g => g.Key == collection.Id)?
|
||||||
|
.Select(g => new CollectionAccessSelection
|
||||||
|
{
|
||||||
|
Id = g.GroupId,
|
||||||
|
HidePasswords = g.HidePasswords,
|
||||||
|
ReadOnly = g.ReadOnly,
|
||||||
|
Manage = g.Manage
|
||||||
|
}).ToList() ?? new List<CollectionAccessSelection>();
|
||||||
|
collection.Users = users
|
||||||
|
.FirstOrDefault(u => u.Key == collection.Id)?
|
||||||
|
.Select(c => new CollectionAccessSelection
|
||||||
|
{
|
||||||
|
Id = c.OrganizationUserId,
|
||||||
|
HidePasswords = c.HidePasswords,
|
||||||
|
ReadOnly = c.ReadOnly,
|
||||||
|
Manage = c.Manage
|
||||||
|
}).ToList() ?? new List<CollectionAccessSelection>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return collections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CollectionAdminDetails> GetByIdWithPermissionsAsync(Guid collectionId, Guid? userId, bool includeAccessRelationships)
|
||||||
|
{
|
||||||
|
using (var connection = new SqlConnection(ConnectionString))
|
||||||
|
{
|
||||||
|
var results = await connection.QueryMultipleAsync(
|
||||||
|
$"[{Schema}].[Collection_ReadByIdWithPermissions]",
|
||||||
|
new { CollectionId = collectionId, UserId = userId, IncludeAccessRelationships = includeAccessRelationships },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
var collectionDetails = await results.ReadFirstOrDefaultAsync<CollectionAdminDetails>();
|
||||||
|
|
||||||
|
if (!includeAccessRelationships) return collectionDetails;
|
||||||
|
|
||||||
|
collectionDetails.Groups = (await results.ReadAsync<CollectionAccessSelection>()).ToList();
|
||||||
|
collectionDetails.Users = (await results.ReadAsync<CollectionAccessSelection>()).ToList();
|
||||||
|
|
||||||
|
return collectionDetails;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task CreateAsync(Collection obj, IEnumerable<CollectionAccessSelection> groups, IEnumerable<CollectionAccessSelection> users)
|
public async Task CreateAsync(Collection obj, IEnumerable<CollectionAccessSelection> groups, IEnumerable<CollectionAccessSelection> users)
|
||||||
{
|
{
|
||||||
obj.SetNewId();
|
obj.SetNewId();
|
||||||
|
@ -370,6 +370,210 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ICollection<CollectionAdminDetails>> GetManyByOrganizationIdWithPermissionsAsync(
|
||||||
|
Guid organizationId, Guid userId, bool includeAccessRelationships)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var query = CollectionAdminDetailsQuery.ByOrganizationId(organizationId, userId).Run(dbContext);
|
||||||
|
|
||||||
|
ICollection<CollectionAdminDetails> collections;
|
||||||
|
|
||||||
|
// SQLite does not support the GROUP BY clause
|
||||||
|
if (dbContext.Database.IsSqlite())
|
||||||
|
{
|
||||||
|
collections = (await query.ToListAsync())
|
||||||
|
.GroupBy(c => new
|
||||||
|
{
|
||||||
|
c.Id,
|
||||||
|
c.OrganizationId,
|
||||||
|
c.Name,
|
||||||
|
c.CreationDate,
|
||||||
|
c.RevisionDate,
|
||||||
|
c.ExternalId
|
||||||
|
}).Select(collectionGroup => new CollectionAdminDetails
|
||||||
|
{
|
||||||
|
Id = collectionGroup.Key.Id,
|
||||||
|
OrganizationId = collectionGroup.Key.OrganizationId,
|
||||||
|
Name = collectionGroup.Key.Name,
|
||||||
|
CreationDate = collectionGroup.Key.CreationDate,
|
||||||
|
RevisionDate = collectionGroup.Key.RevisionDate,
|
||||||
|
ExternalId = collectionGroup.Key.ExternalId,
|
||||||
|
ReadOnly = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.ReadOnly))),
|
||||||
|
HidePasswords =
|
||||||
|
Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
||||||
|
Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),
|
||||||
|
Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned)))
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
collections = await (from c in query
|
||||||
|
group c by new
|
||||||
|
{
|
||||||
|
c.Id,
|
||||||
|
c.OrganizationId,
|
||||||
|
c.Name,
|
||||||
|
c.CreationDate,
|
||||||
|
c.RevisionDate,
|
||||||
|
c.ExternalId
|
||||||
|
}
|
||||||
|
into collectionGroup
|
||||||
|
select new CollectionAdminDetails
|
||||||
|
{
|
||||||
|
Id = collectionGroup.Key.Id,
|
||||||
|
OrganizationId = collectionGroup.Key.OrganizationId,
|
||||||
|
Name = collectionGroup.Key.Name,
|
||||||
|
CreationDate = collectionGroup.Key.CreationDate,
|
||||||
|
RevisionDate = collectionGroup.Key.RevisionDate,
|
||||||
|
ExternalId = collectionGroup.Key.ExternalId,
|
||||||
|
ReadOnly = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.ReadOnly))),
|
||||||
|
HidePasswords =
|
||||||
|
Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
||||||
|
Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),
|
||||||
|
Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned)))
|
||||||
|
}).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!includeAccessRelationships)
|
||||||
|
{
|
||||||
|
return collections;
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups = (from c in collections
|
||||||
|
join cg in dbContext.CollectionGroups on c.Id equals cg.CollectionId
|
||||||
|
group cg by cg.CollectionId into g
|
||||||
|
select g).ToList();
|
||||||
|
|
||||||
|
var users = (from c in collections
|
||||||
|
join cu in dbContext.CollectionUsers on c.Id equals cu.CollectionId
|
||||||
|
group cu by cu.CollectionId into u
|
||||||
|
select u).ToList();
|
||||||
|
|
||||||
|
foreach (var collection in collections)
|
||||||
|
{
|
||||||
|
collection.Groups = groups
|
||||||
|
.FirstOrDefault(g => g.Key == collection.Id)?
|
||||||
|
.Select(g => new CollectionAccessSelection
|
||||||
|
{
|
||||||
|
Id = g.GroupId,
|
||||||
|
HidePasswords = g.HidePasswords,
|
||||||
|
ReadOnly = g.ReadOnly,
|
||||||
|
Manage = g.Manage,
|
||||||
|
}).ToList() ?? new List<CollectionAccessSelection>();
|
||||||
|
collection.Users = users
|
||||||
|
.FirstOrDefault(u => u.Key == collection.Id)?
|
||||||
|
.Select(c => new CollectionAccessSelection
|
||||||
|
{
|
||||||
|
Id = c.OrganizationUserId,
|
||||||
|
HidePasswords = c.HidePasswords,
|
||||||
|
ReadOnly = c.ReadOnly,
|
||||||
|
Manage = c.Manage
|
||||||
|
}).ToList() ?? new List<CollectionAccessSelection>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return collections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CollectionAdminDetails> GetByIdWithPermissionsAsync(Guid collectionId, Guid? userId,
|
||||||
|
bool includeAccessRelationships)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var query = CollectionAdminDetailsQuery.ByCollectionId(collectionId, userId).Run(dbContext);
|
||||||
|
|
||||||
|
CollectionAdminDetails collectionDetails;
|
||||||
|
|
||||||
|
// SQLite does not support the GROUP BY clause
|
||||||
|
if (dbContext.Database.IsSqlite())
|
||||||
|
{
|
||||||
|
collectionDetails = (await query.ToListAsync())
|
||||||
|
.GroupBy(c => new
|
||||||
|
{
|
||||||
|
c.Id,
|
||||||
|
c.OrganizationId,
|
||||||
|
c.Name,
|
||||||
|
c.CreationDate,
|
||||||
|
c.RevisionDate,
|
||||||
|
c.ExternalId
|
||||||
|
}).Select(collectionGroup => new CollectionAdminDetails
|
||||||
|
{
|
||||||
|
Id = collectionGroup.Key.Id,
|
||||||
|
OrganizationId = collectionGroup.Key.OrganizationId,
|
||||||
|
Name = collectionGroup.Key.Name,
|
||||||
|
CreationDate = collectionGroup.Key.CreationDate,
|
||||||
|
RevisionDate = collectionGroup.Key.RevisionDate,
|
||||||
|
ExternalId = collectionGroup.Key.ExternalId,
|
||||||
|
ReadOnly = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.ReadOnly))),
|
||||||
|
HidePasswords =
|
||||||
|
Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
||||||
|
Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),
|
||||||
|
Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned)))
|
||||||
|
}).FirstOrDefault();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
collectionDetails = await (from c in query
|
||||||
|
group c by new
|
||||||
|
{
|
||||||
|
c.Id,
|
||||||
|
c.OrganizationId,
|
||||||
|
c.Name,
|
||||||
|
c.CreationDate,
|
||||||
|
c.RevisionDate,
|
||||||
|
c.ExternalId
|
||||||
|
}
|
||||||
|
into collectionGroup
|
||||||
|
select new CollectionAdminDetails
|
||||||
|
{
|
||||||
|
Id = collectionGroup.Key.Id,
|
||||||
|
OrganizationId = collectionGroup.Key.OrganizationId,
|
||||||
|
Name = collectionGroup.Key.Name,
|
||||||
|
CreationDate = collectionGroup.Key.CreationDate,
|
||||||
|
RevisionDate = collectionGroup.Key.RevisionDate,
|
||||||
|
ExternalId = collectionGroup.Key.ExternalId,
|
||||||
|
ReadOnly = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.ReadOnly))),
|
||||||
|
HidePasswords =
|
||||||
|
Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
||||||
|
Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),
|
||||||
|
Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned)))
|
||||||
|
}).FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!includeAccessRelationships)
|
||||||
|
{
|
||||||
|
return collectionDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupsQuery = from cg in dbContext.CollectionGroups
|
||||||
|
where cg.CollectionId.Equals(collectionId)
|
||||||
|
select new CollectionAccessSelection
|
||||||
|
{
|
||||||
|
Id = cg.GroupId,
|
||||||
|
ReadOnly = cg.ReadOnly,
|
||||||
|
HidePasswords = cg.HidePasswords,
|
||||||
|
Manage = cg.Manage
|
||||||
|
};
|
||||||
|
collectionDetails.Groups = await groupsQuery.ToListAsync();
|
||||||
|
|
||||||
|
var usersQuery = from cg in dbContext.CollectionUsers
|
||||||
|
where cg.CollectionId.Equals(collectionId)
|
||||||
|
select new CollectionAccessSelection
|
||||||
|
{
|
||||||
|
Id = cg.OrganizationUserId,
|
||||||
|
ReadOnly = cg.ReadOnly,
|
||||||
|
HidePasswords = cg.HidePasswords,
|
||||||
|
Manage = cg.Manage
|
||||||
|
};
|
||||||
|
collectionDetails.Users = await usersQuery.ToListAsync();
|
||||||
|
|
||||||
|
return collectionDetails;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<ICollection<CollectionAccessSelection>> GetManyUsersByIdAsync(Guid id)
|
public async Task<ICollection<CollectionAccessSelection>> GetManyUsersByIdAsync(Guid id)
|
||||||
{
|
{
|
||||||
using (var scope = ServiceScopeFactory.CreateScope())
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
@ -0,0 +1,87 @@
|
|||||||
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query to get collection details, including permissions for the specified user if provided.
|
||||||
|
/// </summary>
|
||||||
|
public class CollectionAdminDetailsQuery : IQuery<CollectionAdminDetails>
|
||||||
|
{
|
||||||
|
private readonly Guid? _userId;
|
||||||
|
private readonly Guid? _organizationId;
|
||||||
|
private readonly Guid? _collectionId;
|
||||||
|
|
||||||
|
private CollectionAdminDetailsQuery(Guid? userId, Guid? organizationId, Guid? collectionId)
|
||||||
|
{
|
||||||
|
_userId = userId;
|
||||||
|
_organizationId = organizationId;
|
||||||
|
_collectionId = collectionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual IQueryable<CollectionAdminDetails> Run(DatabaseContext dbContext)
|
||||||
|
{
|
||||||
|
var baseCollectionQuery = from c in dbContext.Collections
|
||||||
|
join ou in dbContext.OrganizationUsers
|
||||||
|
on new { c.OrganizationId, UserId = _userId } equals
|
||||||
|
new { ou.OrganizationId, ou.UserId } into ou_g
|
||||||
|
from ou in ou_g.DefaultIfEmpty()
|
||||||
|
|
||||||
|
join cu in dbContext.CollectionUsers
|
||||||
|
on new { CollectionId = c.Id, OrganizationUserId = ou.Id } equals
|
||||||
|
new { cu.CollectionId, cu.OrganizationUserId } into cu_g
|
||||||
|
from cu in cu_g.DefaultIfEmpty()
|
||||||
|
|
||||||
|
join gu in dbContext.GroupUsers
|
||||||
|
on new { CollectionId = (Guid?)cu.CollectionId, OrganizationUserId = ou.Id } equals
|
||||||
|
new { CollectionId = (Guid?)null, gu.OrganizationUserId } into gu_g
|
||||||
|
from gu in gu_g.DefaultIfEmpty()
|
||||||
|
|
||||||
|
join g in dbContext.Groups
|
||||||
|
on gu.GroupId equals g.Id into g_g
|
||||||
|
from g in g_g.DefaultIfEmpty()
|
||||||
|
|
||||||
|
join cg in dbContext.CollectionGroups
|
||||||
|
on new { CollectionId = c.Id, gu.GroupId } equals
|
||||||
|
new { cg.CollectionId, cg.GroupId } into cg_g
|
||||||
|
from cg in cg_g.DefaultIfEmpty()
|
||||||
|
select new { c, cu, cg };
|
||||||
|
|
||||||
|
if (_organizationId.HasValue)
|
||||||
|
{
|
||||||
|
baseCollectionQuery = baseCollectionQuery.Where(x => x.c.OrganizationId == _organizationId);
|
||||||
|
}
|
||||||
|
else if (_collectionId.HasValue)
|
||||||
|
{
|
||||||
|
baseCollectionQuery = baseCollectionQuery.Where(x => x.c.Id == _collectionId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("OrganizationId or CollectionId must be specified.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseCollectionQuery.Select(x => new CollectionAdminDetails
|
||||||
|
{
|
||||||
|
Id = x.c.Id,
|
||||||
|
OrganizationId = x.c.OrganizationId,
|
||||||
|
Name = x.c.Name,
|
||||||
|
ExternalId = x.c.ExternalId,
|
||||||
|
CreationDate = x.c.CreationDate,
|
||||||
|
RevisionDate = x.c.RevisionDate,
|
||||||
|
ReadOnly = (bool?)x.cu.ReadOnly ?? (bool?)x.cg.ReadOnly ?? false,
|
||||||
|
HidePasswords = (bool?)x.cu.HidePasswords ?? (bool?)x.cg.HidePasswords ?? false,
|
||||||
|
Manage = (bool?)x.cu.Manage ?? (bool?)x.cg.Manage ?? false,
|
||||||
|
Assigned = x.cu != null || x.cg != null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CollectionAdminDetailsQuery ByCollectionId(Guid collectionId, Guid? userId)
|
||||||
|
{
|
||||||
|
return new CollectionAdminDetailsQuery(userId, null, collectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CollectionAdminDetailsQuery ByOrganizationId(Guid organizationId, Guid? userId)
|
||||||
|
{
|
||||||
|
return new CollectionAdminDetailsQuery(userId, organizationId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[Collection_ReadByIdWithPermissions]
|
||||||
|
@CollectionId 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]
|
||||||
|
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.[Id] = @CollectionId
|
||||||
|
GROUP BY
|
||||||
|
C.[Id],
|
||||||
|
C.[OrganizationId],
|
||||||
|
C.[Name],
|
||||||
|
C.[CreationDate],
|
||||||
|
C.[RevisionDate],
|
||||||
|
C.[ExternalId]
|
||||||
|
|
||||||
|
IF (@IncludeAccessRelationships = 1)
|
||||||
|
BEGIN
|
||||||
|
EXEC [dbo].[CollectionGroup_ReadByCollectionId] @CollectionId
|
||||||
|
EXEC [dbo].[CollectionUser_ReadByCollectionId] @CollectionId
|
||||||
|
END
|
||||||
|
END
|
@ -0,0 +1,62 @@
|
|||||||
|
CREATE 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]
|
||||||
|
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
|
||||||
|
GROUP BY
|
||||||
|
C.[Id],
|
||||||
|
C.[OrganizationId],
|
||||||
|
C.[Name],
|
||||||
|
C.[CreationDate],
|
||||||
|
C.[RevisionDate],
|
||||||
|
C.[ExternalId]
|
||||||
|
|
||||||
|
IF (@IncludeAccessRelationships = 1)
|
||||||
|
BEGIN
|
||||||
|
EXEC [dbo].[CollectionGroup_ReadByOrganizationId] @OrganizationId
|
||||||
|
EXEC [dbo].[CollectionUser_ReadByOrganizationId] @OrganizationId
|
||||||
|
END
|
||||||
|
END
|
@ -115,15 +115,20 @@ public class CollectionsControllerTests
|
|||||||
|
|
||||||
await sutProvider.Sut.GetManyWithDetails(organizationAbility.Id);
|
await sutProvider.Sut.GetManyWithDetails(organizationAbility.Id);
|
||||||
|
|
||||||
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdWithAccessAsync(userId, organizationAbility.Id, Arg.Any<bool>());
|
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdWithPermissionsAsync(organizationAbility.Id, userId, true);
|
||||||
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdWithAccessAsync(organizationAbility.Id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task GetOrganizationCollectionsWithGroups_MissingReadAllPermissions_GetsAssignedCollections(
|
public async Task GetOrganizationCollectionsWithGroups_MissingReadAllPermissions_GetsAssignedCollections(
|
||||||
OrganizationAbility organizationAbility, Guid userId, SutProvider<CollectionsController> sutProvider)
|
OrganizationAbility organizationAbility, Guid userId, SutProvider<CollectionsController> sutProvider, List<CollectionAdminDetails> collections)
|
||||||
{
|
{
|
||||||
ArrangeOrganizationAbility(sutProvider, organizationAbility);
|
ArrangeOrganizationAbility(sutProvider, organizationAbility);
|
||||||
|
collections.ForEach(c => c.OrganizationId = organizationAbility.Id);
|
||||||
|
collections.ForEach(c => c.Manage = false);
|
||||||
|
|
||||||
|
var managedCollection = collections.First();
|
||||||
|
managedCollection.Manage = true;
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
|
|
||||||
sutProvider.GetDependency<IAuthorizationService>()
|
sutProvider.GetDependency<IAuthorizationService>()
|
||||||
@ -145,10 +150,16 @@ public class CollectionsControllerTests
|
|||||||
operation.Name == nameof(BulkCollectionOperations.ReadWithAccess))))
|
operation.Name == nameof(BulkCollectionOperations.ReadWithAccess))))
|
||||||
.Returns(AuthorizationResult.Success());
|
.Returns(AuthorizationResult.Success());
|
||||||
|
|
||||||
await sutProvider.Sut.GetManyWithDetails(organizationAbility.Id);
|
sutProvider.GetDependency<ICollectionRepository>()
|
||||||
|
.GetManyByOrganizationIdWithPermissionsAsync(organizationAbility.Id, userId, true)
|
||||||
|
.Returns(collections);
|
||||||
|
|
||||||
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdWithAccessAsync(userId, organizationAbility.Id, Arg.Any<bool>());
|
var response = await sutProvider.Sut.GetManyWithDetails(organizationAbility.Id);
|
||||||
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceive().GetManyByOrganizationIdWithAccessAsync(organizationAbility.Id);
|
|
||||||
|
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdWithPermissionsAsync(organizationAbility.Id, userId, true);
|
||||||
|
Assert.Single(response.Data);
|
||||||
|
Assert.All(response.Data, c => Assert.Equal(organizationAbility.Id, c.OrganizationId));
|
||||||
|
Assert.All(response.Data, c => Assert.Equal(managedCollection.Id, c.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
|
@ -36,6 +36,10 @@ public class CollectionCustomization : ICustomization
|
|||||||
.With(o => o.OrganizationId, orgId)
|
.With(o => o.OrganizationId, orgId)
|
||||||
.WithGuidFromSeed(cd => cd.Id, _collectionIdSeed));
|
.WithGuidFromSeed(cd => cd.Id, _collectionIdSeed));
|
||||||
|
|
||||||
|
fixture.Customize<CollectionAdminDetails>(composer => composer
|
||||||
|
.With(o => o.OrganizationId, orgId)
|
||||||
|
.WithGuidFromSeed(cd => cd.Id, _collectionIdSeed));
|
||||||
|
|
||||||
fixture.Customize<CollectionUser>(c => c
|
fixture.Customize<CollectionUser>(c => c
|
||||||
.WithGuidFromSeed(cu => cu.OrganizationUserId, _userIdSeed)
|
.WithGuidFromSeed(cu => cu.OrganizationUserId, _userIdSeed)
|
||||||
.WithGuidFromSeed(cu => cu.CollectionId, _collectionIdSeed));
|
.WithGuidFromSeed(cu => cu.CollectionId, _collectionIdSeed));
|
||||||
|
@ -0,0 +1,459 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.IntegrationTest.Repositories;
|
||||||
|
|
||||||
|
public class CollectionRepositoryTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Test to ensure that access relationships are retrieved when requested
|
||||||
|
/// </summary>
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task GetByIdWithPermissionsAsync_WithRelationships_Success(IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
ICollectionRepository collectionRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository, IGroupRepository groupRepository)
|
||||||
|
{
|
||||||
|
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 group = await groupRepository.CreateAsync(new Group
|
||||||
|
{
|
||||||
|
Name = "Test Group",
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
});
|
||||||
|
|
||||||
|
var collection = new Collection { Name = "Test Collection", OrganizationId = organization.Id, };
|
||||||
|
|
||||||
|
await collectionRepository.CreateAsync(collection, groups: new[]
|
||||||
|
{
|
||||||
|
new CollectionAccessSelection
|
||||||
|
{
|
||||||
|
Id = group.Id, HidePasswords = false, ReadOnly = true, Manage = false
|
||||||
|
}
|
||||||
|
}, users: new[]
|
||||||
|
{
|
||||||
|
new CollectionAccessSelection()
|
||||||
|
{
|
||||||
|
Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var collectionWithPermissions = await collectionRepository.GetByIdWithPermissionsAsync(collection.Id, user.Id, true);
|
||||||
|
|
||||||
|
Assert.NotNull(collectionWithPermissions);
|
||||||
|
Assert.Equal(1, collectionWithPermissions.Users?.Count());
|
||||||
|
Assert.Equal(1, collectionWithPermissions.Groups?.Count());
|
||||||
|
Assert.True(collectionWithPermissions.Assigned);
|
||||||
|
Assert.True(collectionWithPermissions.Manage);
|
||||||
|
Assert.False(collectionWithPermissions.ReadOnly);
|
||||||
|
Assert.False(collectionWithPermissions.HidePasswords);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test to ensure that a user's explicitly assigned permissions replaces any group permissions
|
||||||
|
/// that user may belong to
|
||||||
|
/// </summary>
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task GetByIdWithPermissionsAsync_UserOverrideGroup_Success(IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
ICollectionRepository collectionRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository, IGroupRepository groupRepository)
|
||||||
|
{
|
||||||
|
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 group = await groupRepository.CreateAsync(new Group
|
||||||
|
{
|
||||||
|
Name = "Test Group",
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assign the test user to the test group
|
||||||
|
await groupRepository.UpdateUsersAsync(group.Id, new[] { orgUser.Id });
|
||||||
|
|
||||||
|
var collection = new Collection { Name = "Test Collection", OrganizationId = organization.Id, };
|
||||||
|
|
||||||
|
await collectionRepository.CreateAsync(collection, groups: new[]
|
||||||
|
{
|
||||||
|
new CollectionAccessSelection
|
||||||
|
{
|
||||||
|
Id = group.Id, HidePasswords = false, ReadOnly = false, Manage = true // Group is Manage
|
||||||
|
}
|
||||||
|
}, users: new[]
|
||||||
|
{
|
||||||
|
new CollectionAccessSelection()
|
||||||
|
{
|
||||||
|
Id = orgUser.Id, HidePasswords = false, ReadOnly = true, Manage = false // User is given ReadOnly (should override group)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var collectionWithPermissions = await collectionRepository.GetByIdWithPermissionsAsync(collection.Id, user.Id, true);
|
||||||
|
|
||||||
|
Assert.NotNull(collectionWithPermissions);
|
||||||
|
Assert.Equal(1, collectionWithPermissions.Users?.Count());
|
||||||
|
Assert.Equal(1, collectionWithPermissions.Groups?.Count());
|
||||||
|
Assert.True(collectionWithPermissions.Assigned);
|
||||||
|
Assert.False(collectionWithPermissions.Manage);
|
||||||
|
Assert.True(collectionWithPermissions.ReadOnly);
|
||||||
|
Assert.False(collectionWithPermissions.HidePasswords);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test to ensure that the returned permissions are the most permissive combination of group permissions when
|
||||||
|
/// multiple groups are assigned to the same collection with different permissions
|
||||||
|
/// </summary>
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task GetByIdWithPermissionsAsync_CombineGroupPermissions_Success(IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
ICollectionRepository collectionRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository, IGroupRepository groupRepository)
|
||||||
|
{
|
||||||
|
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 group = await groupRepository.CreateAsync(new Group
|
||||||
|
{
|
||||||
|
Name = "Test Group",
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
});
|
||||||
|
|
||||||
|
var group2 = await groupRepository.CreateAsync(new Group
|
||||||
|
{
|
||||||
|
Name = "Test Group 2",
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assign the test user to the test groups
|
||||||
|
await groupRepository.UpdateUsersAsync(group.Id, new[] { orgUser.Id });
|
||||||
|
await groupRepository.UpdateUsersAsync(group2.Id, new[] { orgUser.Id });
|
||||||
|
|
||||||
|
var collection = new Collection { Name = "Test Collection", OrganizationId = organization.Id, };
|
||||||
|
|
||||||
|
await collectionRepository.CreateAsync(collection, groups: new[]
|
||||||
|
{
|
||||||
|
new CollectionAccessSelection
|
||||||
|
{
|
||||||
|
Id = group.Id, HidePasswords = false, ReadOnly = true, Manage = false // Group 1 is ReadOnly
|
||||||
|
},
|
||||||
|
new CollectionAccessSelection
|
||||||
|
{
|
||||||
|
Id = group2.Id, HidePasswords = false, ReadOnly = false, Manage = true // Group 2 is Manage
|
||||||
|
}
|
||||||
|
}, users: new List<CollectionAccessSelection>()); // No explicit user permissions for this test
|
||||||
|
|
||||||
|
var collectionWithPermissions = await collectionRepository.GetByIdWithPermissionsAsync(collection.Id, user.Id, true);
|
||||||
|
|
||||||
|
Assert.NotNull(collectionWithPermissions);
|
||||||
|
Assert.Equal(2, collectionWithPermissions.Groups?.Count());
|
||||||
|
Assert.True(collectionWithPermissions.Assigned);
|
||||||
|
|
||||||
|
// Since Group2 is Manage the user should have Manage
|
||||||
|
Assert.True(collectionWithPermissions.Manage);
|
||||||
|
|
||||||
|
// Similarly, ReadOnly and HidePassword should be false
|
||||||
|
Assert.False(collectionWithPermissions.ReadOnly);
|
||||||
|
Assert.False(collectionWithPermissions.HidePasswords);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test to ensure the basic usage works as expected
|
||||||
|
/// </summary>
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task GetManyByOrganizationIdWithPermissionsAsync_Success(IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
ICollectionRepository collectionRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository, IGroupRepository groupRepository)
|
||||||
|
{
|
||||||
|
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 group = await groupRepository.CreateAsync(new Group
|
||||||
|
{
|
||||||
|
Name = "Test Group",
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
});
|
||||||
|
|
||||||
|
var collection1 = new Collection { Name = "Collection 1", OrganizationId = organization.Id, };
|
||||||
|
|
||||||
|
await collectionRepository.CreateAsync(collection1, groups: new[]
|
||||||
|
{
|
||||||
|
new CollectionAccessSelection
|
||||||
|
{
|
||||||
|
Id = group.Id, HidePasswords = false, ReadOnly = true, Manage = false
|
||||||
|
}
|
||||||
|
}, users: new[]
|
||||||
|
{
|
||||||
|
new CollectionAccessSelection()
|
||||||
|
{
|
||||||
|
Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var collection2 = new Collection { Name = "Collection 2", OrganizationId = organization.Id, };
|
||||||
|
|
||||||
|
await collectionRepository.CreateAsync(collection2, null, users: new[]
|
||||||
|
{
|
||||||
|
new CollectionAccessSelection()
|
||||||
|
{
|
||||||
|
Id = orgUser.Id, HidePasswords = false, ReadOnly = true, Manage = false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var collection3 = new Collection { Name = "Collection 3", OrganizationId = organization.Id, };
|
||||||
|
|
||||||
|
await collectionRepository.CreateAsync(collection3, groups: new[]
|
||||||
|
{
|
||||||
|
new CollectionAccessSelection()
|
||||||
|
{
|
||||||
|
Id = group.Id, HidePasswords = false, ReadOnly = false, Manage = true
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
|
|
||||||
|
var collections = await collectionRepository.GetManyByOrganizationIdWithPermissionsAsync(organization.Id, user.Id, true);
|
||||||
|
|
||||||
|
Assert.NotNull(collections);
|
||||||
|
|
||||||
|
collections = collections.OrderBy(c => c.Name).ToList();
|
||||||
|
|
||||||
|
Assert.Collection(collections, c1 =>
|
||||||
|
{
|
||||||
|
Assert.NotNull(c1);
|
||||||
|
Assert.Equal(1, c1.Users?.Count());
|
||||||
|
Assert.Equal(1, c1.Groups?.Count());
|
||||||
|
Assert.True(c1.Assigned);
|
||||||
|
Assert.True(c1.Manage);
|
||||||
|
Assert.False(c1.ReadOnly);
|
||||||
|
Assert.False(c1.HidePasswords);
|
||||||
|
}, c2 =>
|
||||||
|
{
|
||||||
|
Assert.NotNull(c2);
|
||||||
|
Assert.Equal(1, c2.Users?.Count());
|
||||||
|
Assert.Equal(0, c2.Groups?.Count());
|
||||||
|
Assert.True(c2.Assigned);
|
||||||
|
Assert.False(c2.Manage);
|
||||||
|
Assert.True(c2.ReadOnly);
|
||||||
|
Assert.False(c2.HidePasswords);
|
||||||
|
}, c3 =>
|
||||||
|
{
|
||||||
|
Assert.NotNull(c3);
|
||||||
|
Assert.Equal(0, c3.Users?.Count());
|
||||||
|
Assert.Equal(1, c3.Groups?.Count());
|
||||||
|
Assert.False(c3.Assigned);
|
||||||
|
Assert.False(c3.Manage);
|
||||||
|
Assert.False(c3.ReadOnly);
|
||||||
|
Assert.False(c3.HidePasswords);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test to ensure collections assigned to multiple groups do not duplicate in the results
|
||||||
|
/// </summary>
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task GetManyByOrganizationIdWithPermissionsAsync_GroupBy_Success(IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
ICollectionRepository collectionRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository, IGroupRepository groupRepository)
|
||||||
|
{
|
||||||
|
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 group = await groupRepository.CreateAsync(new Group
|
||||||
|
{
|
||||||
|
Name = "Test Group",
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
});
|
||||||
|
|
||||||
|
var group2 = await groupRepository.CreateAsync(new Group
|
||||||
|
{
|
||||||
|
Name = "Test Group 2",
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assign the test user to the test groups
|
||||||
|
await groupRepository.UpdateUsersAsync(group.Id, new[] { orgUser.Id });
|
||||||
|
await groupRepository.UpdateUsersAsync(group2.Id, new[] { orgUser.Id });
|
||||||
|
|
||||||
|
var collection1 = new Collection { Name = "Collection 1", OrganizationId = organization.Id, };
|
||||||
|
|
||||||
|
await collectionRepository.CreateAsync(collection1, groups: new[]
|
||||||
|
{
|
||||||
|
new CollectionAccessSelection
|
||||||
|
{
|
||||||
|
Id = group.Id, HidePasswords = false, ReadOnly = true, Manage = false
|
||||||
|
},
|
||||||
|
}, users: new[]
|
||||||
|
{
|
||||||
|
new CollectionAccessSelection()
|
||||||
|
{
|
||||||
|
Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var collection2 = new Collection { Name = "Collection 2", OrganizationId = organization.Id, };
|
||||||
|
|
||||||
|
await collectionRepository.CreateAsync(collection2, null, users: new[]
|
||||||
|
{
|
||||||
|
new CollectionAccessSelection()
|
||||||
|
{
|
||||||
|
Id = orgUser.Id, HidePasswords = false, ReadOnly = true, Manage = false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var collection3 = new Collection { Name = "Collection 3", OrganizationId = organization.Id, };
|
||||||
|
|
||||||
|
await collectionRepository.CreateAsync(collection3, groups: new[]
|
||||||
|
{
|
||||||
|
new CollectionAccessSelection()
|
||||||
|
{
|
||||||
|
Id = group.Id, HidePasswords = false, ReadOnly = false, Manage = true
|
||||||
|
},
|
||||||
|
new CollectionAccessSelection()
|
||||||
|
{
|
||||||
|
Id = group2.Id, HidePasswords = false, ReadOnly = true, Manage = false
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
|
|
||||||
|
var collections = await collectionRepository.GetManyByOrganizationIdWithPermissionsAsync(organization.Id, user.Id, true);
|
||||||
|
|
||||||
|
Assert.NotNull(collections);
|
||||||
|
|
||||||
|
Assert.Equal(3, collections.Count);
|
||||||
|
|
||||||
|
collections = collections.OrderBy(c => c.Name).ToList();
|
||||||
|
|
||||||
|
Assert.Collection(collections, c1 =>
|
||||||
|
{
|
||||||
|
Assert.NotNull(c1);
|
||||||
|
Assert.Equal(1, c1.Users?.Count());
|
||||||
|
Assert.Equal(1, c1.Groups?.Count());
|
||||||
|
Assert.True(c1.Assigned);
|
||||||
|
Assert.True(c1.Manage);
|
||||||
|
Assert.False(c1.ReadOnly);
|
||||||
|
Assert.False(c1.HidePasswords);
|
||||||
|
}, c2 =>
|
||||||
|
{
|
||||||
|
Assert.NotNull(c2);
|
||||||
|
Assert.Equal(1, c2.Users?.Count());
|
||||||
|
Assert.Equal(0, c2.Groups?.Count());
|
||||||
|
Assert.True(c2.Assigned);
|
||||||
|
Assert.False(c2.Manage);
|
||||||
|
Assert.True(c2.ReadOnly);
|
||||||
|
Assert.False(c2.HidePasswords);
|
||||||
|
}, c3 =>
|
||||||
|
{
|
||||||
|
Assert.NotNull(c3);
|
||||||
|
Assert.Equal(0, c3.Users?.Count());
|
||||||
|
Assert.Equal(2, c3.Groups?.Count());
|
||||||
|
Assert.True(c3.Assigned); // User is a member of both Groups
|
||||||
|
Assert.True(c3.Manage); // Group 2 is Manage
|
||||||
|
Assert.False(c3.ReadOnly);
|
||||||
|
Assert.False(c3.HidePasswords);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,126 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadByIdWithPermissions]
|
||||||
|
@CollectionId 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]
|
||||||
|
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.[Id] = @CollectionId
|
||||||
|
GROUP BY
|
||||||
|
C.[Id],
|
||||||
|
C.[OrganizationId],
|
||||||
|
C.[Name],
|
||||||
|
C.[CreationDate],
|
||||||
|
C.[RevisionDate],
|
||||||
|
C.[ExternalId]
|
||||||
|
|
||||||
|
IF (@IncludeAccessRelationships = 1)
|
||||||
|
BEGIN
|
||||||
|
EXEC [dbo].[CollectionGroup_ReadByCollectionId] @CollectionId
|
||||||
|
EXEC [dbo].[CollectionUser_ReadByCollectionId] @CollectionId
|
||||||
|
END
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
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]
|
||||||
|
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
|
||||||
|
GROUP BY
|
||||||
|
C.[Id],
|
||||||
|
C.[OrganizationId],
|
||||||
|
C.[Name],
|
||||||
|
C.[CreationDate],
|
||||||
|
C.[RevisionDate],
|
||||||
|
C.[ExternalId]
|
||||||
|
|
||||||
|
IF (@IncludeAccessRelationships = 1)
|
||||||
|
BEGIN
|
||||||
|
EXEC [dbo].[CollectionGroup_ReadByOrganizationId] @OrganizationId
|
||||||
|
EXEC [dbo].[CollectionUser_ReadByOrganizationId] @OrganizationId
|
||||||
|
END
|
||||||
|
END
|
||||||
|
GO
|
Loading…
Reference in New Issue
Block a user