Skip to content

Commit

Permalink
PM-16261 move ImportCiphersAsync to the tools team (#5245)
Browse files Browse the repository at this point in the history
* PM-16261 move ImportCiphersAsync to the tools team and create services using CQRS design pattern

* PM-16261 fix renaming methods and add unit tests for succes and bad request exception

* PM-16261 clean up old code from test
  • Loading branch information
prograhamming authored Jan 24, 2025
1 parent 36c8a97 commit 99a1dbb
Show file tree
Hide file tree
Showing 10 changed files with 416 additions and 218 deletions.
2 changes: 2 additions & 0 deletions src/Api/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Tools.ImportFeatures;
using Bit.Core.Tools.ReportFeatures;


Expand Down Expand Up @@ -175,6 +176,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddCoreLocalizationServices();
services.AddBillingOperations();
services.AddReportingServices();
services.AddImportServices();

// Authorization Handlers
services.AddAuthorizationHandlers();
Expand Down
13 changes: 6 additions & 7 deletions src/Api/Tools/Controllers/ImportCiphersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Vault.Services;
using Bit.Core.Tools.ImportFeatures.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -17,31 +17,30 @@ namespace Bit.Api.Tools.Controllers;
[Authorize("Application")]
public class ImportCiphersController : Controller
{
private readonly ICipherService _cipherService;
private readonly IUserService _userService;
private readonly ICurrentContext _currentContext;
private readonly ILogger<ImportCiphersController> _logger;
private readonly GlobalSettings _globalSettings;
private readonly ICollectionRepository _collectionRepository;
private readonly IAuthorizationService _authorizationService;
private readonly IImportCiphersCommand _importCiphersCommand;

public ImportCiphersController(
ICipherService cipherService,
IUserService userService,
ICurrentContext currentContext,
ILogger<ImportCiphersController> logger,
GlobalSettings globalSettings,
ICollectionRepository collectionRepository,
IAuthorizationService authorizationService,
IOrganizationRepository organizationRepository)
IImportCiphersCommand importCiphersCommand)
{
_cipherService = cipherService;
_userService = userService;
_currentContext = currentContext;
_logger = logger;
_globalSettings = globalSettings;
_collectionRepository = collectionRepository;
_authorizationService = authorizationService;
_importCiphersCommand = importCiphersCommand;
}

[HttpPost("import")]
Expand All @@ -57,7 +56,7 @@ public async Task PostImport([FromBody] ImportCiphersRequestModel model)
var userId = _userService.GetProperUserId(User).Value;
var folders = model.Folders.Select(f => f.ToFolder(userId)).ToList();
var ciphers = model.Ciphers.Select(c => c.ToCipherDetails(userId, false)).ToList();
await _cipherService.ImportCiphersAsync(folders, ciphers, model.FolderRelationships);
await _importCiphersCommand.ImportIntoIndividualVaultAsync(folders, ciphers, model.FolderRelationships);
}

[HttpPost("import-organization")]
Expand Down Expand Up @@ -85,7 +84,7 @@ public async Task PostImport([FromQuery] string organizationId,

var userId = _userService.GetProperUserId(User).Value;
var ciphers = model.Ciphers.Select(l => l.ToOrganizationCipherDetails(orgId)).ToList();
await _cipherService.ImportCiphersAsync(collections, ciphers, model.CollectionRelationships, userId);
await _importCiphersCommand.ImportIntoOrganizationalVaultAsync(collections, ciphers, model.CollectionRelationships, userId);
}

private async Task<bool> CheckOrgImportPermission(List<Collection> collections, Guid orgId)
Expand Down
199 changes: 199 additions & 0 deletions src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.ImportFeatures.Interfaces;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories;

namespace Bit.Core.Tools.ImportFeatures;

public class ImportCiphersCommand : IImportCiphersCommand
{
private readonly ICipherRepository _cipherRepository;
private readonly IFolderRepository _folderRepository;
private readonly IPushNotificationService _pushService;
private readonly IPolicyService _policyService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly ICollectionRepository _collectionRepository;
private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext;


public ImportCiphersCommand(
ICipherRepository cipherRepository,
IFolderRepository folderRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IPushNotificationService pushService,
IPolicyService policyService,
IReferenceEventService referenceEventService,
ICurrentContext currentContext)
{
_cipherRepository = cipherRepository;
_folderRepository = folderRepository;
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
_collectionRepository = collectionRepository;
_pushService = pushService;
_policyService = policyService;
_referenceEventService = referenceEventService;
_currentContext = currentContext;
}


public async Task ImportIntoIndividualVaultAsync(
List<Folder> folders,
List<CipherDetails> ciphers,
IEnumerable<KeyValuePair<int, int>> folderRelationships)
{
var userId = folders.FirstOrDefault()?.UserId ?? ciphers.FirstOrDefault()?.UserId;

// Make sure the user can save new ciphers to their personal vault
var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value, PolicyType.PersonalOwnership);
if (anyPersonalOwnershipPolicies)
{
throw new BadRequestException("You cannot import items into your personal vault because you are " +
"a member of an organization which forbids it.");
}

foreach (var cipher in ciphers)
{
cipher.SetNewId();

if (cipher.UserId.HasValue && cipher.Favorite)
{
cipher.Favorites = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":\"true\"}}";
}
}

var userfoldersIds = (await _folderRepository.GetManyByUserIdAsync(userId ?? Guid.Empty)).Select(f => f.Id).ToList();

//Assign id to the ones that don't exist in DB
//Need to keep the list order to create the relationships
List<Folder> newFolders = new List<Folder>();
foreach (var folder in folders)
{
if (!userfoldersIds.Contains(folder.Id))
{
folder.SetNewId();
newFolders.Add(folder);
}
}

// Create the folder associations based on the newly created folder ids
foreach (var relationship in folderRelationships)
{
var cipher = ciphers.ElementAtOrDefault(relationship.Key);
var folder = folders.ElementAtOrDefault(relationship.Value);

if (cipher == null || folder == null)
{
continue;
}

cipher.Folders = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":" +
$"\"{folder.Id.ToString().ToUpperInvariant()}\"}}";
}

// Create it all
await _cipherRepository.CreateAsync(ciphers, newFolders);

// push
if (userId.HasValue)
{
await _pushService.PushSyncVaultAsync(userId.Value);
}
}

public async Task ImportIntoOrganizationalVaultAsync(
List<Collection> collections,
List<CipherDetails> ciphers,
IEnumerable<KeyValuePair<int, int>> collectionRelationships,
Guid importingUserId)
{
var org = collections.Count > 0 ?
await _organizationRepository.GetByIdAsync(collections[0].OrganizationId) :
await _organizationRepository.GetByIdAsync(ciphers.FirstOrDefault(c => c.OrganizationId.HasValue).OrganizationId.Value);
var importingOrgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, importingUserId);

if (collections.Count > 0 && org != null && org.MaxCollections.HasValue)
{
var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(org.Id);
if (org.MaxCollections.Value < (collectionCount + collections.Count))
{
throw new BadRequestException("This organization can only have a maximum of " +
$"{org.MaxCollections.Value} collections.");
}
}

// Init. ids for ciphers
foreach (var cipher in ciphers)
{
cipher.SetNewId();
}

var organizationCollectionsIds = (await _collectionRepository.GetManyByOrganizationIdAsync(org.Id)).Select(c => c.Id).ToList();

//Assign id to the ones that don't exist in DB
//Need to keep the list order to create the relationships
var newCollections = new List<Collection>();
var newCollectionUsers = new List<CollectionUser>();

foreach (var collection in collections)
{
if (!organizationCollectionsIds.Contains(collection.Id))
{
collection.SetNewId();
newCollections.Add(collection);
newCollectionUsers.Add(new CollectionUser
{
CollectionId = collection.Id,
OrganizationUserId = importingOrgUser.Id,
Manage = true
});
}
}

// Create associations based on the newly assigned ids
var collectionCiphers = new List<CollectionCipher>();
foreach (var relationship in collectionRelationships)
{
var cipher = ciphers.ElementAtOrDefault(relationship.Key);
var collection = collections.ElementAtOrDefault(relationship.Value);

if (cipher == null || collection == null)
{
continue;
}

collectionCiphers.Add(new CollectionCipher
{
CipherId = cipher.Id,
CollectionId = collection.Id
});
}

// Create it all
await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers, newCollectionUsers);

// push
await _pushService.PushSyncVaultAsync(importingUserId);


if (org != null)
{
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.VaultImported, org, _currentContext));
}
}
}
12 changes: 12 additions & 0 deletions src/Core/Tools/ImportFeatures/ImportServiceCollectionExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Bit.Core.Tools.ImportFeatures.Interfaces;
using Microsoft.Extensions.DependencyInjection;

namespace Bit.Core.Tools.ImportFeatures;

public static class ImportServiceCollectionExtension
{
public static void AddImportServices(this IServiceCollection services)
{
services.AddScoped<IImportCiphersCommand, ImportCiphersCommand>();
}
}
14 changes: 14 additions & 0 deletions src/Core/Tools/ImportFeatures/Interfaces/IImportCiphersCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Bit.Core.Entities;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Models.Data;

namespace Bit.Core.Tools.ImportFeatures.Interfaces;

public interface IImportCiphersCommand
{
Task ImportIntoIndividualVaultAsync(List<Folder> folders, List<CipherDetails> ciphers,
IEnumerable<KeyValuePair<int, int>> folderRelationships);

Task ImportIntoOrganizationalVaultAsync(List<Collection> collections, List<CipherDetails> ciphers,
IEnumerable<KeyValuePair<int, int>> collectionRelationships, Guid importingUserId);
}
4 changes: 0 additions & 4 deletions src/Core/Vault/Services/ICipherService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,6 @@ Task ShareAsync(Cipher originalCipher, Cipher cipher, Guid organizationId, IEnum
Task ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> ciphers, Guid organizationId,
IEnumerable<Guid> collectionIds, Guid sharingUserId);
Task SaveCollectionsAsync(Cipher cipher, IEnumerable<Guid> collectionIds, Guid savingUserId, bool orgAdmin);
Task ImportCiphersAsync(List<Folder> folders, List<CipherDetails> ciphers,
IEnumerable<KeyValuePair<int, int>> folderRelationships);
Task ImportCiphersAsync(List<Collection> collections, List<CipherDetails> ciphers,
IEnumerable<KeyValuePair<int, int>> collectionRelationships, Guid importingUserId);
Task SoftDeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false);
Task SoftDeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);
Task RestoreAsync(Cipher cipher, Guid restoringUserId, bool orgAdmin = false);
Expand Down
Loading

0 comments on commit 99a1dbb

Please sign in to comment.