mirror of
https://github.com/bitwarden/server.git
synced 2025-07-02 16:42:50 -05: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:
@ -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);
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user