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:
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
30
src/Core/Vault/Queries/IOrganizationCiphersQuery.cs
Normal file
30
src/Core/Vault/Queries/IOrganizationCiphersQuery.cs
Normal 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);
|
||||
}
|
79
src/Core/Vault/Queries/OrganizationCiphersQuery.cs
Normal file
79
src/Core/Vault/Queries/OrganizationCiphersQuery.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
19
src/Core/Vault/VaultServiceCollectionExtensions.cs
Normal file
19
src/Core/Vault/VaultServiceCollectionExtensions.cs
Normal 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>();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user