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)
|
||||
{
|
||||
// New flexible collections logic
|
||||
var (collection, access) = await _collectionRepository.GetByIdWithAccessAsync(id);
|
||||
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ReadWithAccess)).Succeeded;
|
||||
var collectionAdminDetails =
|
||||
await _collectionRepository.GetByIdWithPermissionsAsync(id, _currentContext.UserId, true);
|
||||
|
||||
var authorized = (await _authorizationService.AuthorizeAsync(User, collectionAdminDetails, BulkCollectionOperations.ReadWithAccess)).Succeeded;
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new CollectionAccessDetailsResponseModel(collection, access.Groups, access.Users);
|
||||
return new CollectionAccessDetailsResponseModel(collectionAdminDetails);
|
||||
}
|
||||
|
||||
private async Task<ListResponseModel<CollectionAccessDetailsResponseModel>> GetManyWithDetails_vNext(Guid orgId)
|
||||
{
|
||||
// We always need to know which collections the current user is assigned to
|
||||
var assignedOrgCollections = await _collectionRepository
|
||||
.GetManyByUserIdWithAccessAsync(_currentContext.UserId.Value, orgId, true);
|
||||
var allOrgCollections = await _collectionRepository.GetManyByOrganizationIdWithPermissionsAsync(
|
||||
orgId, _currentContext.UserId.Value, true);
|
||||
|
||||
var readAllAuthorized =
|
||||
(await _authorizationService.AuthorizeAsync(User, CollectionOperations.ReadAllWithAccess(orgId))).Succeeded;
|
||||
if (readAllAuthorized)
|
||||
{
|
||||
// The user can view all collections, but they may not always be assigned to all of them
|
||||
var allOrgCollections = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(orgId);
|
||||
|
||||
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)
|
||||
})
|
||||
return new ListResponseModel<CollectionAccessDetailsResponseModel>(
|
||||
allOrgCollections.Select(c => new CollectionAccessDetailsResponseModel(c))
|
||||
);
|
||||
}
|
||||
|
||||
// Filter the assigned collections to only return those where the user has Manage permission
|
||||
var manageableOrgCollections = assignedOrgCollections.Where(c => c.Item1.Manage).ToList();
|
||||
// Filter collections to only return those where the user has Manage permission
|
||||
var manageableOrgCollections = allOrgCollections.Where(c => c.Manage).ToList();
|
||||
|
||||
return new ListResponseModel<CollectionAccessDetailsResponseModel>(manageableOrgCollections.Select(c =>
|
||||
new CollectionAccessDetailsResponseModel(c.Item1, c.Item2.Groups, c.Item2.Users)
|
||||
{
|
||||
Assigned = true // Mapping from manageableOrgCollections implies they're all assigned
|
||||
})
|
||||
);
|
||||
new CollectionAccessDetailsResponseModel(c)
|
||||
));
|
||||
}
|
||||
|
||||
private async Task<ListResponseModel<CollectionResponseModel>> GetByOrgId_vNext(Guid orgId)
|
||||
@ -629,7 +620,7 @@ public class CollectionsController : Controller
|
||||
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);
|
||||
|
||||
@ -644,21 +635,18 @@ public class CollectionsController : Controller
|
||||
|
||||
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
|
||||
var userCollectionDetails = await _collectionRepository.GetByIdAsync(collection.Id,
|
||||
_currentContext.UserId.Value, await FlexibleCollectionsIsEnabledAsync(collection.OrganizationId));
|
||||
// If we have a user, fetch the latest collection permission details
|
||||
var collectionWithPermissions = await _collectionRepository.GetByIdWithPermissionsAsync(collection.Id, _currentContext.UserId.Value, false);
|
||||
|
||||
return userCollectionDetails == null
|
||||
? new CollectionResponseModel(collection)
|
||||
: new CollectionDetailsResponseModel(userCollectionDetails);
|
||||
return new CollectionAccessDetailsResponseModel(collectionWithPermissions);
|
||||
}
|
||||
|
||||
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 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());
|
||||
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
|
||||
var updatedCollectionDetails = await _collectionRepository.GetByIdAsync(id, _currentContext.UserId.Value, await FlexibleCollectionsIsEnabledAsync(collection.OrganizationId));
|
||||
// If we have a user, fetch the latest collection permission details
|
||||
var collectionWithPermissions = await _collectionRepository.GetByIdWithPermissionsAsync(collection.Id, _currentContext.UserId.Value, false);
|
||||
|
||||
return updatedCollectionDetails == null
|
||||
? new CollectionResponseModel(collection)
|
||||
: new CollectionDetailsResponseModel(updatedCollectionDetails);
|
||||
return new CollectionAccessDetailsResponseModel(collectionWithPermissions);
|
||||
}
|
||||
|
||||
private async Task PutUsers_vNext(Guid id, IEnumerable<SelectionReadOnlyRequestModel> model)
|
||||
|
@ -26,8 +26,15 @@ public class CollectionResponseModel : ResponseModel
|
||||
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
|
||||
{
|
||||
/// <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)
|
||||
: base(collectionDetails, "collectionDetails")
|
||||
{
|
||||
@ -43,6 +50,27 @@ public class CollectionDetailsResponseModel : 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)
|
||||
: base(collection, "collectionAccessDetails")
|
||||
{
|
||||
@ -50,6 +78,21 @@ public class CollectionAccessDetailsResponseModel : CollectionResponseModel
|
||||
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> Users { get; set; }
|
||||
|
||||
@ -57,4 +100,8 @@ public class CollectionAccessDetailsResponseModel : CollectionResponseModel
|
||||
/// True if the acting user is explicitly assigned to the collection
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Collection information that includes permission details for a particular user
|
||||
/// </summary>
|
||||
public class CollectionDetails : Collection
|
||||
{
|
||||
public bool ReadOnly { get; set; }
|
||||
|
@ -6,14 +6,64 @@ namespace Bit.Core.Repositories;
|
||||
public interface ICollectionRepository : IRepository<Collection, Guid>
|
||||
{
|
||||
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);
|
||||
|
||||
/// <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);
|
||||
|
||||
/// <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);
|
||||
|
||||
/// <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);
|
||||
|
||||
/// <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);
|
||||
|
||||
/// <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<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);
|
||||
|
||||
/// <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 ReplaceAsync(Collection obj, IEnumerable<CollectionAccessSelection> groups, IEnumerable<CollectionAccessSelection> users);
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdWithAccessAsync(userId, organizationAbility.Id, Arg.Any<bool>());
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdWithAccessAsync(organizationAbility.Id);
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdWithPermissionsAsync(organizationAbility.Id, userId, true);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
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);
|
||||
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<IAuthorizationService>()
|
||||
@ -145,10 +150,16 @@ public class CollectionsControllerTests
|
||||
operation.Name == nameof(BulkCollectionOperations.ReadWithAccess))))
|
||||
.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>());
|
||||
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceive().GetManyByOrganizationIdWithAccessAsync(organizationAbility.Id);
|
||||
var response = await sutProvider.Sut.GetManyWithDetails(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]
|
||||
|
@ -36,6 +36,10 @@ public class CollectionCustomization : ICustomization
|
||||
.With(o => o.OrganizationId, orgId)
|
||||
.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
|
||||
.WithGuidFromSeed(cu => cu.OrganizationUserId, _userIdSeed)
|
||||
.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