mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 05:00:19 -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:
parent
058f1822ed
commit
636f716d62
@ -9,6 +9,7 @@ using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -17,6 +18,7 @@ using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
using Bit.Core.Vault.Queries;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Bit.Core.Vault.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -41,10 +43,14 @@ public class CiphersController : Controller
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly Version _cipherKeyEncryptionMinimumVersion = new Version(Constants.CipherKeyEncryptionMinimumVersion);
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IOrganizationCiphersQuery _organizationCiphersQuery;
|
||||
|
||||
private bool UseFlexibleCollections =>
|
||||
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections);
|
||||
|
||||
private bool UseFlexibleCollectionsV1 =>
|
||||
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1);
|
||||
|
||||
public CiphersController(
|
||||
ICipherRepository cipherRepository,
|
||||
ICollectionCipherRepository collectionCipherRepository,
|
||||
@ -55,7 +61,8 @@ public class CiphersController : Controller
|
||||
ICurrentContext currentContext,
|
||||
ILogger<CiphersController> logger,
|
||||
GlobalSettings globalSettings,
|
||||
IFeatureService featureService)
|
||||
IFeatureService featureService,
|
||||
IOrganizationCiphersQuery organizationCiphersQuery)
|
||||
{
|
||||
_cipherRepository = cipherRepository;
|
||||
_collectionCipherRepository = collectionCipherRepository;
|
||||
@ -67,6 +74,7 @@ public class CiphersController : Controller
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
_featureService = featureService;
|
||||
_organizationCiphersQuery = organizationCiphersQuery;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -230,26 +238,152 @@ public class CiphersController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("organization-details")]
|
||||
public async Task<ListResponseModel<CipherMiniDetailsResponseModel>> GetOrganizationCollections(
|
||||
string organizationId)
|
||||
public async Task<ListResponseModel<CipherMiniDetailsResponseModel>> GetOrganizationCiphers(Guid organizationId)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var orgIdGuid = new Guid(organizationId);
|
||||
// Flexible Collections Logic
|
||||
if (UseFlexibleCollectionsV1)
|
||||
{
|
||||
return await GetAllOrganizationCiphersAsync(organizationId);
|
||||
}
|
||||
|
||||
(IEnumerable<CipherOrganizationDetails> orgCiphers, Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict) = await _cipherService.GetOrganizationCiphers(userId, orgIdGuid);
|
||||
// Pre-Flexible Collections Logic
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
|
||||
(IEnumerable<CipherOrganizationDetails> orgCiphers, Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict) = await _cipherService.GetOrganizationCiphers(userId, organizationId);
|
||||
|
||||
var responses = orgCiphers.Select(c => new CipherMiniDetailsResponseModel(c, _globalSettings,
|
||||
collectionCiphersGroupDict, c.OrganizationUseTotp));
|
||||
|
||||
var providerId = await _currentContext.ProviderIdForOrg(orgIdGuid);
|
||||
var providerId = await _currentContext.ProviderIdForOrg(organizationId);
|
||||
if (providerId.HasValue)
|
||||
{
|
||||
await _providerService.LogProviderAccessToOrganizationAsync(orgIdGuid);
|
||||
await _providerService.LogProviderAccessToOrganizationAsync(organizationId);
|
||||
}
|
||||
|
||||
return new ListResponseModel<CipherMiniDetailsResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpGet("organization-details/assigned")]
|
||||
public async Task<ListResponseModel<CipherDetailsResponseModel>> GetAssignedOrganizationCiphers(Guid organizationId)
|
||||
{
|
||||
if (!UseFlexibleCollectionsV1)
|
||||
{
|
||||
throw new FeatureUnavailableException();
|
||||
}
|
||||
|
||||
if (!await CanAccessOrganizationCiphersAsync(organizationId) || !_currentContext.UserId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var ciphers = await _organizationCiphersQuery.GetOrganizationCiphersForUser(organizationId, _currentContext.UserId.Value);
|
||||
|
||||
if (await CanAccessUnassignedCiphersAsync(organizationId))
|
||||
{
|
||||
var unassignedCiphers = await _organizationCiphersQuery.GetUnassignedOrganizationCiphers(organizationId);
|
||||
ciphers = ciphers.Concat(unassignedCiphers.Select(c => new CipherDetailsWithCollections(c, null)
|
||||
{
|
||||
// Users that can access unassigned ciphers can also edit them
|
||||
Edit = true,
|
||||
ViewPassword = true,
|
||||
}));
|
||||
}
|
||||
|
||||
var responses = ciphers.Select(c => new CipherDetailsResponseModel(c, _globalSettings));
|
||||
|
||||
return new ListResponseModel<CipherDetailsResponseModel>(responses);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all ciphers belonging to the organization if the user has access to All ciphers.
|
||||
/// </summary>
|
||||
/// <exception cref="NotFoundException"></exception>
|
||||
private async Task<ListResponseModel<CipherMiniDetailsResponseModel>> GetAllOrganizationCiphersAsync(Guid organizationId)
|
||||
{
|
||||
if (!await CanAccessAllCiphersAsync(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var allOrganizationCiphers = await _organizationCiphersQuery.GetAllOrganizationCiphers(organizationId);
|
||||
|
||||
var allOrganizationCipherResponses =
|
||||
allOrganizationCiphers.Select(c =>
|
||||
new CipherMiniDetailsResponseModel(c, _globalSettings, c.OrganizationUseTotp)
|
||||
);
|
||||
|
||||
return new ListResponseModel<CipherMiniDetailsResponseModel>(allOrganizationCipherResponses);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TODO: Move this to its own authorization handler or equivalent service - AC-2062
|
||||
/// </summary>
|
||||
private async Task<bool> CanAccessAllCiphersAsync(Guid organizationId)
|
||||
{
|
||||
var org = _currentContext.GetOrganization(organizationId);
|
||||
|
||||
if (org is
|
||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||
{ Permissions.AccessImportExport: true })
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Provider users can access all ciphers in V1 (to change later)
|
||||
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TODO: Move this to its own authorization handler or equivalent service - AC-2062
|
||||
/// </summary>
|
||||
private async Task<bool> CanAccessOrganizationCiphersAsync(Guid organizationId)
|
||||
{
|
||||
var org = _currentContext.GetOrganization(organizationId);
|
||||
|
||||
// The user has a relationship with the organization;
|
||||
// they can access its ciphers in collections they've been assigned
|
||||
if (org is not null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Provider users can still access organization ciphers in V1 (to change later)
|
||||
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TODO: Move this to its own authorization handler or equivalent service - AC-2062
|
||||
/// </summary>
|
||||
private async Task<bool> CanAccessUnassignedCiphersAsync(Guid organizationId)
|
||||
{
|
||||
var org = _currentContext.GetOrganization(organizationId);
|
||||
|
||||
if (org is
|
||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||
{ Permissions.EditAnyCollection: true })
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Provider users can access all ciphers in V1 (to change later)
|
||||
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[HttpPut("{id}/partial")]
|
||||
[HttpPost("{id}/partial")]
|
||||
public async Task<CipherResponseModel> PutPartial(Guid id, [FromBody] CipherPartialRequestModel model)
|
||||
|
@ -127,6 +127,12 @@ public class CipherDetailsResponseModel : CipherResponseModel
|
||||
CollectionIds = collectionCiphers?.Select(c => c.CollectionId) ?? new List<Guid>();
|
||||
}
|
||||
|
||||
public CipherDetailsResponseModel(CipherDetailsWithCollections cipher, GlobalSettings globalSettings, string obj = "cipherDetails")
|
||||
: base(cipher, globalSettings, obj)
|
||||
{
|
||||
CollectionIds = cipher.CollectionIds ?? new List<Guid>();
|
||||
}
|
||||
|
||||
public IEnumerable<Guid> CollectionIds { get; set; }
|
||||
}
|
||||
|
||||
@ -146,5 +152,12 @@ public class CipherMiniDetailsResponseModel : CipherMiniResponseModel
|
||||
}
|
||||
}
|
||||
|
||||
public CipherMiniDetailsResponseModel(CipherOrganizationDetailsWithCollections cipher,
|
||||
GlobalSettings globalSettings, bool orgUseTotp, string obj = "cipherMiniDetails")
|
||||
: base(cipher, globalSettings, orgUseTotp, obj)
|
||||
{
|
||||
CollectionIds = cipher.CollectionIds ?? new List<Guid>();
|
||||
}
|
||||
|
||||
public IEnumerable<Guid> CollectionIds { get; set; }
|
||||
}
|
||||
|
@ -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>();
|
||||
}
|
||||
}
|
@ -122,6 +122,19 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<CipherOrganizationDetails>> GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(Guid organizationId)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<CipherOrganizationDetails>(
|
||||
$"[{Schema}].[CipherOrganizationDetails_ReadUnassignedByOrganizationId]",
|
||||
new { OrganizationId = organizationId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
{
|
||||
cipher.SetNewId();
|
||||
|
@ -349,6 +349,17 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<CipherOrganizationDetails>> GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(Guid organizationId)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var query = new CipherOrganizationDetailsReadByOrganizationIdQuery(organizationId, true);
|
||||
var data = await query.Run(dbContext).ToListAsync();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<CipherDetails>> GetManyByUserIdAsync(Guid userId, bool useFlexibleCollections, bool withOrganizations = true)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
|
@ -6,10 +6,17 @@ namespace Bit.Infrastructure.EntityFramework.Repositories.Vault.Queries;
|
||||
public class CipherOrganizationDetailsReadByOrganizationIdQuery : IQuery<CipherOrganizationDetails>
|
||||
{
|
||||
private readonly Guid _organizationId;
|
||||
private readonly bool _unassignedOnly;
|
||||
|
||||
public CipherOrganizationDetailsReadByOrganizationIdQuery(Guid organizationId)
|
||||
/// <summary>
|
||||
/// Query for retrieving ciphers organization details by organization id
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The id of the organization to query</param>
|
||||
/// <param name="unassignedOnly">Only include ciphers that are not assigned to any collection</param>
|
||||
public CipherOrganizationDetailsReadByOrganizationIdQuery(Guid organizationId, bool unassignedOnly = false)
|
||||
{
|
||||
_organizationId = organizationId;
|
||||
_unassignedOnly = unassignedOnly;
|
||||
}
|
||||
public virtual IQueryable<CipherOrganizationDetails> Run(DatabaseContext dbContext)
|
||||
{
|
||||
@ -33,6 +40,18 @@ public class CipherOrganizationDetailsReadByOrganizationIdQuery : IQuery<CipherO
|
||||
DeletedDate = c.DeletedDate,
|
||||
OrganizationUseTotp = o.UseTotp,
|
||||
};
|
||||
|
||||
if (_unassignedOnly)
|
||||
{
|
||||
var collectionCipherIds = from cc in dbContext.CollectionCiphers
|
||||
join c in dbContext.Collections
|
||||
on cc.CollectionId equals c.Id
|
||||
where c.OrganizationId == _organizationId
|
||||
select cc.CipherId;
|
||||
|
||||
query = query.Where(c => !collectionCipherIds.Contains(c.Id));
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault;
|
||||
using Bit.Core.Vault.Services;
|
||||
using Bit.Infrastructure.Dapper;
|
||||
using Bit.Infrastructure.EntityFramework;
|
||||
@ -144,6 +145,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<ISendService, SendService>();
|
||||
services.AddLoginServices();
|
||||
services.AddScoped<IOrganizationDomainService, OrganizationDomainService>();
|
||||
services.AddVaultServices();
|
||||
}
|
||||
|
||||
public static void AddTokenizers(this IServiceCollection services)
|
||||
|
@ -0,0 +1,26 @@
|
||||
CREATE PROCEDURE [dbo].[CipherOrganizationDetails_ReadUnassignedByOrganizationId]
|
||||
@OrganizationId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
C.*,
|
||||
CASE
|
||||
WHEN O.[UseTotp] = 1 THEN 1
|
||||
ELSE 0
|
||||
END [OrganizationUseTotp]
|
||||
FROM
|
||||
[dbo].[CipherView] C
|
||||
LEFT JOIN
|
||||
[dbo].[OrganizationView] O ON O.[Id] = C.[OrganizationId]
|
||||
LEFT JOIN
|
||||
[dbo].[CollectionCipher] CC ON C.[Id] = CC.[CipherId]
|
||||
LEFT JOIN
|
||||
[dbo].[Collection] S ON S.[Id] = CC.[CollectionId]
|
||||
AND S.[OrganizationId] = C.[OrganizationId]
|
||||
WHERE
|
||||
C.[UserId] IS NULL
|
||||
AND C.[OrganizationId] = @OrganizationId
|
||||
AND CC.[CipherId] IS NULL
|
||||
END
|
@ -0,0 +1,27 @@
|
||||
CREATE OR ALTER PROCEDURE [dbo].[CipherOrganizationDetails_ReadUnassignedByOrganizationId]
|
||||
@OrganizationId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
C.*,
|
||||
CASE
|
||||
WHEN O.[UseTotp] = 1 THEN 1
|
||||
ELSE 0
|
||||
END [OrganizationUseTotp]
|
||||
FROM
|
||||
[dbo].[CipherView] C
|
||||
LEFT JOIN
|
||||
[dbo].[OrganizationView] O ON O.[Id] = C.[OrganizationId]
|
||||
LEFT JOIN
|
||||
[dbo].[CollectionCipher] CC ON C.[Id] = CC.[CipherId]
|
||||
LEFT JOIN
|
||||
[dbo].[Collection] S ON S.[Id] = CC.[CollectionId]
|
||||
AND S.[OrganizationId] = C.[OrganizationId]
|
||||
WHERE
|
||||
C.[UserId] IS NULL
|
||||
AND C.[OrganizationId] = @OrganizationId
|
||||
AND CC.[CipherId] IS NULL
|
||||
END
|
||||
GO
|
Loading…
x
Reference in New Issue
Block a user