Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SM-68] Add API endpoints for getting, creating, and editing secrets #2201

Merged
merged 4 commits into from
Aug 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Bit.Commercial.Core.SecretManagerFeatures.Secrets;
using Bit.Commercial.Core.SecretManagerFeatures.Secrets.Interfaces;
using Microsoft.Extensions.DependencyInjection;

namespace Bit.Commercial.Core.SecretManagerFeatures
{
public static class SecretManagerCollectionExtensions
{
public static void AddSecretManagerServices(this IServiceCollection services)
{
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Bit.Commercial.Core.SecretManagerFeatures.Secrets.Interfaces;
using Bit.Core.Entities;
using Bit.Core.Repositories;

namespace Bit.Commercial.Core.SecretManagerFeatures.Secrets
{
public class CreateSecretCommand : ICreateSecretCommand
{
private readonly ISecretRepository _secretRepository;

public CreateSecretCommand(ISecretRepository secretRepository)
{
_secretRepository = secretRepository;
}

public async Task<Secret> CreateAsync(Secret secret)
{
return await _secretRepository.CreateAsync(secret);
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Bit.Core.Entities;

namespace Bit.Commercial.Core.SecretManagerFeatures.Secrets.Interfaces
{
public interface ICreateSecretCommand
{
Task<Secret> CreateAsync(Secret secret);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Bit.Core.Entities;

namespace Bit.Commercial.Core.SecretManagerFeatures.Secrets.Interfaces
{
public interface IUpdateSecretCommand
{
Task<Secret> UpdateAsync(Secret secret);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Bit.Commercial.Core.SecretManagerFeatures.Secrets.Interfaces;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;

namespace Bit.Commercial.Core.SecretManagerFeatures.Secrets
{
public class UpdateSecretCommand : IUpdateSecretCommand
{
private readonly ISecretRepository _secretRepository;

public UpdateSecretCommand(ISecretRepository secretRepository)
{
_secretRepository = secretRepository;
}

public async Task<Secret> UpdateAsync(Secret secret)
{
if (secret.Id == default(Guid))
{
throw new BadRequestException("Cannot update secret, secret does not exist.");
}

var existingSecret = await _secretRepository.GetByIdAsync(secret.Id);
if (existingSecret == null)
{
throw new NotFoundException();
}

secret.OrganizationId = existingSecret.OrganizationId;
secret.CreationDate = existingSecret.CreationDate;
secret.DeletedDate = existingSecret.DeletedDate;
secret.RevisionDate = DateTime.UtcNow;

await _secretRepository.ReplaceAsync(secret);
return secret;
}
}
}

Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Bit.Commercial.Core.Services;
using Bit.Commercial.Core.SecretManagerFeatures;
using Bit.Commercial.Core.Services;
using Bit.Core.Services;
using Microsoft.Extensions.DependencyInjection;

Expand All @@ -9,6 +10,7 @@ public static class ServiceCollectionExtensions
public static void AddCommCoreServices(this IServiceCollection services)
{
services.AddScoped<IProviderService, ProviderService>();
services.AddSecretManagerServices();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Bit.Commercial.Core.SecretManagerFeatures.Secrets;
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using NSubstitute;
using Xunit;

namespace Bit.Commercial.Core.Test.SecretManagerFeatures.Secrets
{
[SutProviderCustomize]
public class CreateSecretCommandTests
{
[Theory]
[BitAutoData]
public async Task CreateAsync_CallsCreate(Secret data,
SutProvider<CreateSecretCommand> sutProvider)
{
await sutProvider.Sut.CreateAsync(data);

await sutProvider.GetDependency<ISecretRepository>().Received(1)
.CreateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using Bit.Commercial.Core.SecretManagerFeatures.Secrets;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using NSubstitute;
using Xunit;

namespace Bit.Commercial.Core.Test.SecretManagerFeatures.Secrets
{
[SutProviderCustomize]
public class UpdateSecretCommandTests
{
[Theory]
[BitAutoData]
public async Task UpdateAsync_DefaultGuidId_ThrowsNotFound(Secret data, SutProvider<UpdateSecretCommand> sutProvider)
{
data.Id = new Guid();

var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateAsync(data));

Assert.Contains("Cannot update secret, secret does not exist.", exception.Message);
await sutProvider.GetDependency<ISecretRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}

[Theory]
[BitAutoData]
public async Task UpdateAsync_SecretDoesNotExist_ThrowsNotFound(Secret data, SutProvider<UpdateSecretCommand> sutProvider)
{
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data));

await sutProvider.GetDependency<ISecretRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}

[Theory]
[BitAutoData]
public async Task UpdateAsync_CallsReplaceAsync(Secret data, SutProvider<UpdateSecretCommand> sutProvider)
{
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(data.Id).Returns(data);
await sutProvider.Sut.UpdateAsync(data);

await sutProvider.GetDependency<ISecretRepository>().Received(1)
.ReplaceAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));
}

[Theory]
[BitAutoData]
public async Task UpdateAsync_DoesNotModifyOrganizationId(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider)
{
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);

var updatedOrgId = Guid.NewGuid();
var secretUpdate = new Secret()
{
OrganizationId = updatedOrgId,
Id = existingSecret.Id,
Key = existingSecret.Key,
};

var result = await sutProvider.Sut.UpdateAsync(secretUpdate);

Assert.Equal(existingSecret.OrganizationId, result.OrganizationId);
Assert.NotEqual(existingSecret.OrganizationId, updatedOrgId);
}

[Theory]
[BitAutoData]
public async Task UpdateAsync_DoesNotModifyCreationDate(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider)
{
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);

var updatedCreationDate = DateTime.UtcNow;
var secretUpdate = new Secret()
{
CreationDate = updatedCreationDate,
Id = existingSecret.Id,
Key = existingSecret.Key,
};

var result = await sutProvider.Sut.UpdateAsync(secretUpdate);

Assert.Equal(existingSecret.CreationDate, result.CreationDate);
Assert.NotEqual(existingSecret.CreationDate, updatedCreationDate);
}

[Theory]
[BitAutoData]
public async Task UpdateAsync_DoesNotModifyDeletionDate(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider)
{
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);

var updatedDeletionDate = DateTime.UtcNow;
var secretUpdate = new Secret()
{
DeletedDate = updatedDeletionDate,
Id = existingSecret.Id,
Key = existingSecret.Key,
};

var result = await sutProvider.Sut.UpdateAsync(secretUpdate);

Assert.Equal(existingSecret.DeletedDate, result.DeletedDate);
Assert.NotEqual(existingSecret.DeletedDate, updatedDeletionDate);
}


[Theory]
[BitAutoData]
public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider)
{
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);

var updatedRevisionDate = DateTime.UtcNow.AddDays(10);
var secretUpdate = new Secret()
{
RevisionDate = updatedRevisionDate,
Id = existingSecret.Id,
Key = existingSecret.Key,
};

var result = await sutProvider.Sut.UpdateAsync(secretUpdate);

Assert.NotEqual(existingSecret.RevisionDate, result.RevisionDate);
AssertHelper.AssertRecent(result.RevisionDate);
}
}
}

54 changes: 48 additions & 6 deletions src/Api/Controllers/SecretsController.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using Bit.Api.Models.Response;
using Bit.Api.SecretManagerFeatures.Models.Request;
using Bit.Api.SecretManagerFeatures.Models.Response;
using Bit.Api.Utilities;
using Bit.Commercial.Core.SecretManagerFeatures.Secrets.Interfaces;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -10,18 +13,57 @@ namespace Bit.Api.Controllers
public class SecretsController : Controller
{
private readonly ISecretRepository _secretRepository;
private readonly ICreateSecretCommand _createSecretCommand;
private readonly IUpdateSecretCommand _updateSecretCommand;

public SecretsController(ISecretRepository secretRepository)
public SecretsController(ISecretRepository secretRepository, ICreateSecretCommand createSecretCommand, IUpdateSecretCommand updateSecretCommand)
{
_secretRepository = secretRepository;
_createSecretCommand = createSecretCommand;
_updateSecretCommand = updateSecretCommand;
}

[HttpGet("organizations/{orgId}/secrets")]
public async Task<ListResponseModel<SecretResponseModel>> GetAsync([FromRoute] Guid orgId)
[HttpGet("organizations/{organizationId}/secrets")]
public async Task<ListResponseModel<SecretIdentifierResponseModel>> GetSecretsByOrganizationAsync([FromRoute] Guid organizationId)
{
var results = await _secretRepository.GetManyByOrganizationIdAsync(orgId);
var responses = results.Select(secret => new SecretResponseModel(secret));
return new ListResponseModel<SecretResponseModel>(responses);
var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId);
if (secrets == null || !secrets.Any())
{
throw new NotFoundException();
}
var responses = secrets.Select(secret => new SecretIdentifierResponseModel(secret));
return new ListResponseModel<SecretIdentifierResponseModel>(responses);
}


[HttpGet("secrets/{id}")]
public async Task<SecretResponseModel> GetSecretAsync([FromRoute] Guid id)
{
var secret = await _secretRepository.GetByIdAsync(id);
if (secret == null)
{
throw new NotFoundException();
}
return new SecretResponseModel(secret);
}

[HttpPost("organizations/{organizationId}/secrets")]
public async Task<SecretResponseModel> CreateSecretAsync([FromRoute] Guid organizationId, [FromBody] SecretCreateRequestModel createRequest)
{
if (organizationId != createRequest.OrganizationId)
{
throw new BadRequestException("Organization ID does not match.");
}

var result = await _createSecretCommand.CreateAsync(createRequest.ToSecret());
return new SecretResponseModel(result);
}

[HttpPut("secrets/{id}")]
public async Task<SecretResponseModel> UpdateSecretAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest)
{
var result = await _updateSecretCommand.UpdateAsync(updateRequest.ToSecret(id));
return new SecretResponseModel(result);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Entities;
using Bit.Core.Utilities;

namespace Bit.Api.SecretManagerFeatures.Models.Request
{
public class SecretCreateRequestModel : IValidatableObject
{
[Required]
public Guid OrganizationId { get; set; }

[Required]
[EncryptedString]
public string Key { get; set; }

[Required]
[EncryptedString]
public string Value { get; set; }

[Required]
[EncryptedString]
public string Note { get; set; }

public Secret ToSecret()
{
return new Secret()
{
OrganizationId = this.OrganizationId,
Key = this.Key,
Value = this.Value,
Note = this.Note,
DeletedDate = null,
};
}

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (OrganizationId == default(Guid))
{
yield return new ValidationResult("Organization ID is required.");
}
}
}
}

Loading