1
0
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:
Shane Melton
2024-05-03 06:33:06 -07:00
committed by GitHub
parent 25c87214ff
commit d965166a37
14 changed files with 1232 additions and 45 deletions

View File

@ -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())

View File

@ -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);
}
}