1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05:00

SM-365: Add Export & Import Functionality for SM (#2591)

* SM-365: Add Export endpoint

* SM-365: Add SM Import/Export support

* SM-365: Fix DI and add temp NoAccessCheck

* SM-365: Add access checks to import / export

* SM-365: dotnet format

* SM-365: Fix import bugs

* SM-365: Fix import bug with EF & refactor based on PR comments

* SM-365: Update access permissions in export

* SM-365: Address PR comments

* SM-365: Refactor for readability and PR comments
This commit is contained in:
Colton Hurst
2023-02-14 09:24:31 -05:00
committed by GitHub
parent 109d915d9e
commit 5836c87bb4
12 changed files with 453 additions and 1 deletions

View File

@ -0,0 +1,66 @@
using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.Porting.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.SecretsManager.Controllers;
[SecretsManager]
public class SecretsManagerPortingController : Controller
{
private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
private readonly IUserService _userService;
private readonly IImportCommand _importCommand;
private readonly ICurrentContext _currentContext;
public SecretsManagerPortingController(ISecretRepository secretRepository, IProjectRepository projectRepository, IUserService userService, IImportCommand importCommand, ICurrentContext currentContext)
{
_secretRepository = secretRepository;
_projectRepository = projectRepository;
_userService = userService;
_importCommand = importCommand;
_currentContext = currentContext;
}
[HttpGet("sm/{organizationId}/export")]
public async Task<SMExportResponseModel> Export([FromRoute] Guid organizationId, [FromRoute] string format = "json")
{
if (!await _currentContext.OrganizationAdmin(organizationId))
{
throw new UnauthorizedAccessException();
}
var userId = _userService.GetProperUserId(User).Value;
var projects = await _projectRepository.GetManyByOrganizationIdAsync(organizationId, userId, AccessClientType.NoAccessCheck);
var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId);
if (projects == null && secrets == null)
{
throw new NotFoundException();
}
return new SMExportResponseModel(projects, secrets);
}
[HttpPost("sm/{organizationId}/import")]
public async Task Import([FromRoute] Guid organizationId, [FromBody] SMImportRequestModel importRequest)
{
if (!await _currentContext.OrganizationAdmin(organizationId))
{
throw new UnauthorizedAccessException();
}
if (importRequest.Projects?.Count() > 1000 || importRequest.Secrets?.Count() > 6000)
{
throw new BadRequestException("You cannot import this much data at once, the limit is 1000 projects and 6000 secrets.");
}
await _importCommand.ImportAsync(organizationId, importRequest.ToSMImport());
}
}

View File

@ -0,0 +1,70 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.SecretsManager.Commands.Porting;
using Bit.Core.Utilities;
namespace Bit.Api.SecretsManager.Models.Request;
public class SMImportRequestModel
{
public IEnumerable<InnerProjectImportRequestModel> Projects { get; set; }
public IEnumerable<InnerSecretImportRequestModel> Secrets { get; set; }
public class InnerProjectImportRequestModel
{
public InnerProjectImportRequestModel() { }
[Required]
public Guid Id { get; set; }
[Required]
[EncryptedString]
[EncryptedStringLength(1000)]
public string Name { get; set; }
}
public class InnerSecretImportRequestModel
{
public InnerSecretImportRequestModel() { }
[Required]
public Guid Id { get; set; }
[Required]
[EncryptedString]
[EncryptedStringLength(1000)]
public string Key { get; set; }
[Required]
[EncryptedString]
[EncryptedStringLength(1000)]
public string Value { get; set; }
[Required]
[EncryptedString]
[EncryptedStringLength(1000)]
public string Note { get; set; }
[Required]
public IEnumerable<Guid> ProjectIds { get; set; }
}
public SMImport ToSMImport()
{
return new SMImport
{
Projects = Projects?.Select(p => new SMImport.InnerProject
{
Id = p.Id,
Name = p.Name,
}),
Secrets = Secrets?.Select(s => new SMImport.InnerSecret
{
Id = s.Id,
Key = s.Key,
Value = s.Value,
Note = s.Note,
ProjectIds = s.ProjectIds,
}),
};
}
}

View File

@ -0,0 +1,46 @@
using Bit.Core.Models.Api;
using Bit.Core.SecretsManager.Entities;
namespace Bit.Api.SecretsManager.Models.Response;
public class SMExportResponseModel : ResponseModel
{
public SMExportResponseModel(IEnumerable<Project> projects, IEnumerable<Secret> secrets, string obj = "SecretsManagerExportResponseModel") : base(obj)
{
Secrets = secrets?.Select(s => new InnerSecretExportResponseModel(s));
Projects = projects?.Select(p => new InnerProjectExportResponseModel(p));
}
public IEnumerable<InnerProjectExportResponseModel> Projects { get; set; }
public IEnumerable<InnerSecretExportResponseModel> Secrets { get; set; }
public class InnerProjectExportResponseModel
{
public InnerProjectExportResponseModel(Project project)
{
Id = project.Id;
Name = project.Name;
}
public Guid Id { get; set; }
public string Name { get; set; }
}
public class InnerSecretExportResponseModel
{
public InnerSecretExportResponseModel(Secret secret)
{
Id = secret.Id;
Key = secret.Key;
Value = secret.Value;
Note = secret.Note;
ProjectIds = secret.Projects?.Select(p => p.Id);
}
public Guid Id { get; set; }
public string Key { get; set; }
public string Value { get; set; }
public string Note { get; set; }
public IEnumerable<Guid> ProjectIds { get; set; }
}
}

View File

@ -0,0 +1,50 @@
using Bit.Core.Models.Api;
using Bit.Core.SecretsManager.Commands.Porting;
namespace Bit.Api.SecretsManager.Models.Response;
public class SMImportResponseModel : ResponseModel
{
public SMImportResponseModel(SMImport import, string obj = "SecretsManagerImportResponseModel") : base(obj)
{
Projects = import.Projects?.Select(p => new InnerProjectImportResponseModel(p));
Secrets = import.Secrets?.Select(s => new InnerSecretImportResponseModel(s));
}
public IEnumerable<InnerProjectImportResponseModel> Projects { get; set; }
public IEnumerable<InnerSecretImportResponseModel> Secrets { get; set; }
public class InnerProjectImportResponseModel
{
public InnerProjectImportResponseModel() { }
public InnerProjectImportResponseModel(SMImport.InnerProject project)
{
Id = project.Id;
Name = project.Name;
}
public Guid Id { get; set; }
public string Name { get; set; }
}
public class InnerSecretImportResponseModel
{
public InnerSecretImportResponseModel() { }
public InnerSecretImportResponseModel(SMImport.InnerSecret secret)
{
Id = secret.Id;
Key = secret.Key;
Value = secret.Value;
Note = secret.Note;
ProjectIds = secret.ProjectIds;
}
public Guid Id { get; set; }
public string Key { get; set; }
public string Value { get; set; }
public string Note { get; set; }
public IEnumerable<Guid> ProjectIds { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace Bit.Core.SecretsManager.Commands.Porting.Interfaces;
public interface IImportCommand
{
Task ImportAsync(Guid organizationId, SMImport import);
SMImport AssignNewIds(SMImport import);
}

View File

@ -0,0 +1,41 @@
namespace Bit.Core.SecretsManager.Commands.Porting;
public class SMImport
{
public IEnumerable<InnerProject> Projects { get; set; }
public IEnumerable<InnerSecret> Secrets { get; set; }
public class InnerProject
{
public InnerProject() { }
public InnerProject(Core.SecretsManager.Entities.Project project)
{
Id = project.Id;
Name = project.Name;
}
public Guid Id { get; set; }
public string Name { get; set; }
}
public class InnerSecret
{
public InnerSecret() { }
public InnerSecret(Core.SecretsManager.Entities.Secret secret)
{
Id = secret.Id;
Key = secret.Key;
Value = secret.Value;
Note = secret.Note;
ProjectIds = secret.Projects != null && secret.Projects.Any() ? secret.Projects.Select(p => p.Id) : null;
}
public Guid Id { get; set; }
public string Key { get; set; }
public string Value { get; set; }
public string Note { get; set; }
public IEnumerable<Guid> ProjectIds { get; set; }
}
}

View File

@ -11,6 +11,7 @@ public interface IProjectRepository
Task<Project> CreateAsync(Project project);
Task ReplaceAsync(Project project);
Task DeleteManyByIdAsync(IEnumerable<Guid> ids);
Task<IEnumerable<Project>> ImportAsync(IEnumerable<Project> projects);
Task<bool> UserHasReadAccessToProject(Guid id, Guid userId);
Task<bool> UserHasWriteAccessToProject(Guid id, Guid userId);
}

View File

@ -11,4 +11,6 @@ public interface ISecretRepository
Task<Secret> CreateAsync(Secret secret);
Task<Secret> UpdateAsync(Secret secret);
Task SoftDeleteManyByIdAsync(IEnumerable<Guid> ids);
Task HardDeleteManyByIdAsync(IEnumerable<Guid> ids);
Task<IEnumerable<Secret>> ImportAsync(IEnumerable<Secret> secrets);
}