1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-08 03:15:07 -05:00

[AC-1124] Restrict admins from accessing items in Collections tab (#3676)

* [AC-1124] Add GetManyUnassignedOrganizationDetailsByOrganizationIdAsync to the CipherRepository

* [AC-1124] Introduce IOrganizationCiphersQuery.cs to replace some CipherService queries

* [AC-1124] Add additional CipherDetails model that includes CollectionIds

* [AC-1124] Update CiphersController and response models
- Add new endpoint for assigned ciphers
- Update existing endpoint to only return all ciphers when feature flag is enabled the user has access

* [AC-1124] Add migration script

* [AC-1124] Add follow up ticket for Todos

* [AC-1124] Fix feature service usage after merge with main

* [AC-1124] Optimize unassigned ciphers query

* [AC-1124] Update migration script date

* [AC-1124] Update migration script date

* [AC-1124] Formatting
This commit is contained in:
Shane Melton
2024-02-08 14:07:58 -08:00
committed by GitHub
parent 058f1822ed
commit 636f716d62
14 changed files with 470 additions and 11 deletions

View File

@ -1,4 +1,6 @@
namespace Bit.Core.Vault.Models.Data;
using Bit.Core.Entities;
namespace Bit.Core.Vault.Models.Data;
public class CipherDetails : CipherOrganizationDetails
{
@ -7,3 +9,57 @@ public class CipherDetails : CipherOrganizationDetails
public bool Edit { get; set; }
public bool ViewPassword { get; set; }
}
public class CipherDetailsWithCollections : CipherDetails
{
public CipherDetailsWithCollections(
CipherDetails cipher,
Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict)
{
Id = cipher.Id;
UserId = cipher.UserId;
OrganizationId = cipher.OrganizationId;
Type = cipher.Type;
Data = cipher.Data;
Favorites = cipher.Favorites;
Folders = cipher.Folders;
Attachments = cipher.Attachments;
CreationDate = cipher.CreationDate;
RevisionDate = cipher.RevisionDate;
DeletedDate = cipher.DeletedDate;
Reprompt = cipher.Reprompt;
Key = cipher.Key;
FolderId = cipher.FolderId;
Favorite = cipher.Favorite;
Edit = cipher.Edit;
ViewPassword = cipher.ViewPassword;
CollectionIds = collectionCiphersGroupDict.TryGetValue(Id, out var value)
? value.Select(cc => cc.CollectionId)
: Array.Empty<Guid>();
}
public CipherDetailsWithCollections(CipherOrganizationDetails cipher, Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict)
{
Id = cipher.Id;
UserId = cipher.UserId;
OrganizationId = cipher.OrganizationId;
Type = cipher.Type;
Data = cipher.Data;
Favorites = cipher.Favorites;
Folders = cipher.Folders;
Attachments = cipher.Attachments;
CreationDate = cipher.CreationDate;
RevisionDate = cipher.RevisionDate;
DeletedDate = cipher.DeletedDate;
Reprompt = cipher.Reprompt;
Key = cipher.Key;
OrganizationUseTotp = cipher.OrganizationUseTotp;
CollectionIds = collectionCiphersGroupDict != null && collectionCiphersGroupDict.TryGetValue(Id, out var value)
? value.Select(cc => cc.CollectionId)
: Array.Empty<Guid>();
}
public IEnumerable<Guid> CollectionIds { get; set; }
}

View File

@ -1,4 +1,5 @@
using Bit.Core.Vault.Entities;
using Bit.Core.Entities;
using Bit.Core.Vault.Entities;
namespace Bit.Core.Vault.Models.Data;
@ -6,3 +7,31 @@ public class CipherOrganizationDetails : Cipher
{
public bool OrganizationUseTotp { get; set; }
}
public class CipherOrganizationDetailsWithCollections : CipherOrganizationDetails
{
public CipherOrganizationDetailsWithCollections(
CipherOrganizationDetails cipher,
Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict)
{
Id = cipher.Id;
UserId = cipher.UserId;
OrganizationId = cipher.OrganizationId;
Type = cipher.Type;
Data = cipher.Data;
Favorites = cipher.Favorites;
Folders = cipher.Folders;
Attachments = cipher.Attachments;
CreationDate = cipher.CreationDate;
RevisionDate = cipher.RevisionDate;
DeletedDate = cipher.DeletedDate;
Reprompt = cipher.Reprompt;
Key = cipher.Key;
OrganizationUseTotp = cipher.OrganizationUseTotp;
CollectionIds = collectionCiphersGroupDict.TryGetValue(Id, out var value)
? value.Select(cc => cc.CollectionId)
: Array.Empty<Guid>();
}
public IEnumerable<Guid> CollectionIds { get; set; }
}

View File

@ -0,0 +1,30 @@
using Bit.Core.Exceptions;
using Bit.Core.Vault.Models.Data;
namespace Bit.Core.Vault.Queries;
/// <summary>
/// Helper queries for retrieving cipher details belonging to an organization including collection information.
/// </summary>
/// <remarks>It does not perform any internal authorization checks.</remarks>
public interface IOrganizationCiphersQuery
{
/// <summary>
/// Returns ciphers belonging to the organization that the user has been assigned to via collections.
/// </summary>
/// <exception cref="FeatureUnavailableException"></exception>
public Task<IEnumerable<CipherDetailsWithCollections>> GetOrganizationCiphersForUser(Guid organizationId, Guid userId);
/// <summary>
/// Returns all ciphers belonging to the organization.
/// </summary>
/// <param name="organizationId"></param>
/// <exception cref="FeatureUnavailableException"></exception>
public Task<IEnumerable<CipherOrganizationDetailsWithCollections>> GetAllOrganizationCiphers(Guid organizationId);
/// <summary>
/// Returns ciphers belonging to the organization that are not assigned to any collection.
/// </summary>
/// <exception cref="FeatureUnavailableException"></exception>
Task<IEnumerable<CipherOrganizationDetails>> GetUnassignedOrganizationCiphers(Guid organizationId);
}

View File

@ -0,0 +1,79 @@
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories;
namespace Bit.Core.Vault.Queries;
public class OrganizationCiphersQuery : IOrganizationCiphersQuery
{
private readonly ICipherRepository _cipherRepository;
private readonly ICollectionCipherRepository _collectionCipherRepository;
private readonly IFeatureService _featureService;
private bool FlexibleCollectionsV1Enabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1);
public OrganizationCiphersQuery(ICipherRepository cipherRepository, ICollectionCipherRepository collectionCipherRepository, IFeatureService featureService)
{
_cipherRepository = cipherRepository;
_collectionCipherRepository = collectionCipherRepository;
_featureService = featureService;
}
/// <summary>
/// Returns ciphers belonging to the organization that the user has been assigned to via collections.
/// </summary>
public async Task<IEnumerable<CipherDetailsWithCollections>> GetOrganizationCiphersForUser(Guid organizationId, Guid userId)
{
if (!FlexibleCollectionsV1Enabled)
{
// Flexible collections is OFF, should not be using this query
throw new FeatureUnavailableException("Flexible collections is OFF when it should be ON.");
}
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, useFlexibleCollections: true, withOrganizations: true);
var orgCiphers = ciphers.Where(c => c.OrganizationId == organizationId).ToList();
var orgCipherIds = orgCiphers.Select(c => c.Id);
var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(organizationId);
var collectionCiphersGroupDict = collectionCiphers
.Where(c => orgCipherIds.Contains(c.CipherId))
.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
return orgCiphers.Select(c => new CipherDetailsWithCollections(c, collectionCiphersGroupDict));
}
/// <summary>
/// Returns all ciphers belonging to the organization.
/// </summary>
/// <param name="organizationId"></param>
public async Task<IEnumerable<CipherOrganizationDetailsWithCollections>> GetAllOrganizationCiphers(Guid organizationId)
{
if (!FlexibleCollectionsV1Enabled)
{
// Flexible collections is OFF, should not be using this query
throw new FeatureUnavailableException("Flexible collections is OFF when it should be ON.");
}
var orgCiphers = await _cipherRepository.GetManyOrganizationDetailsByOrganizationIdAsync(organizationId);
var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(organizationId);
var collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
return orgCiphers.Select(c => new CipherOrganizationDetailsWithCollections(c, collectionCiphersGroupDict));
}
/// <summary>
/// Returns ciphers belonging to the organization that are not assigned to any collection.
/// </summary>
public async Task<IEnumerable<CipherOrganizationDetails>> GetUnassignedOrganizationCiphers(Guid organizationId)
{
if (!FlexibleCollectionsV1Enabled)
{
// Flexible collections is OFF, should not be using this query
throw new FeatureUnavailableException("Flexible collections is OFF when it should be ON.");
}
return await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organizationId);
}
}

View File

@ -15,6 +15,7 @@ public interface ICipherRepository : IRepository<Cipher, Guid>
Task<bool> GetCanEditByIdAsync(Guid userId, Guid cipherId);
Task<ICollection<CipherDetails>> GetManyByUserIdAsync(Guid userId, bool useFlexibleCollections, bool withOrganizations = true);
Task<ICollection<Cipher>> GetManyByOrganizationIdAsync(Guid organizationId);
Task<ICollection<CipherOrganizationDetails>> GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(Guid organizationId);
Task CreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds);
Task CreateAsync(CipherDetails cipher);
Task CreateAsync(CipherDetails cipher, IEnumerable<Guid> collectionIds);

View File

@ -0,0 +1,19 @@
using Bit.Core.Vault.Queries;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Vault;
public static class VaultServiceCollectionExtensions
{
public static IServiceCollection AddVaultServices(this IServiceCollection services)
{
services.AddVaultQueries();
return services;
}
private static void AddVaultQueries(this IServiceCollection services)
{
services.AddScoped<IOrganizationCiphersQuery, OrganizationCiphersQuery>();
}
}