Skip to content
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
19 changes: 12 additions & 7 deletions src/KeyConnector/Controllers/UserKeysController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
}

[HttpPost]
public async Task<IActionResult> Post([FromBody] UserKeyRequestModel model)

Check warning on line 55 in src/KeyConnector/Controllers/UserKeysController.cs

View workflow job for this annotation

GitHub Actions / Sonar / Quality scan

ModelState.IsValid should be checked in controller actions.

Check warning on line 55 in src/KeyConnector/Controllers/UserKeysController.cs

View workflow job for this annotation

GitHub Actions / Sonar / Quality scan

ModelState.IsValid should be checked in controller actions.
{
var userId = GetProperUserId().Value;
var user = await _userKeyRepository.ReadAsync(userId);
Expand All @@ -70,20 +70,25 @@
}

[HttpPut]
public async Task<IActionResult> Put([FromBody] UserKeyRequestModel model)

Check warning on line 73 in src/KeyConnector/Controllers/UserKeysController.cs

View workflow job for this annotation

GitHub Actions / Sonar / Quality scan

ModelState.IsValid should be checked in controller actions.

Check warning on line 73 in src/KeyConnector/Controllers/UserKeysController.cs

View workflow job for this annotation

GitHub Actions / Sonar / Quality scan

ModelState.IsValid should be checked in controller actions.
{
var userId = GetProperUserId().Value;
var user = await _userKeyRepository.ReadAsync(userId);
if (user != null)
if (user == null)
{
return new BadRequestResult();
user = new UserKeyModel
{
Id = userId,
Key = await _cryptoService.AesEncryptToB64Async(model.Key)
};
await _userKeyRepository.CreateAsync(user);
}
user = new UserKeyModel
else
{
Id = userId,
Key = await _cryptoService.AesEncryptToB64Async(model.Key)
};
await _userKeyRepository.UpdateAsync(user);
user.Key = await _cryptoService.AesEncryptToB64Async(model.Key);
user.RevisionDate = DateTime.UtcNow;
await _userKeyRepository.UpdateAsync(user);
}
return new OkResult();
}

Expand Down
14 changes: 14 additions & 0 deletions src/KeyConnector/Repositories/JsonFile/UserKeyRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ public override async Task CreateAsync(UserKeyModel item)
await collection.InsertOneAsync(new JsonUserKeyModel(item));
}

public override async Task UpdateAsync(UserKeyModel item)
{
var collection = DataStore.GetCollection<JsonUserKeyModel>(CollectionName);
var idString = item.Id.ToString();
await collection.ReplaceOneAsync(e => e.Id == idString, new JsonUserKeyModel(item));
}

public override async Task DeleteAsync(Guid id)
{
var collection = DataStore.GetCollection<JsonUserKeyModel>(CollectionName);
var idString = id.ToString();
await collection.DeleteOneAsync(e => e.Id == idString);
}

// New model is required since JsonFlatFileDataStore doesn't handle Guid id types
public class JsonUserKeyModel : BaseUserKeyModel
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
ο»Ώusing System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Bit.KeyConnector.Models;
using Bit.KeyConnector.Repositories;
using Bit.KeyConnector.Services;
using KeyConnector.Tests.Helpers;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace KeyConnector.Tests.Controllers;

public class UserKeysControllerIntegrationTests : IClassFixture<KeyConnectorWebApplicationFactory>
{
private readonly HttpClient _client;
private readonly IUserKeyRepository _userKeyRepository;
private readonly ICryptoService _cryptoService;

public UserKeysControllerIntegrationTests(KeyConnectorWebApplicationFactory factory)
{
_client = factory.CreateClient();
var scope = factory.Services;
_userKeyRepository = scope.GetRequiredService<IUserKeyRepository>();
_cryptoService = scope.GetRequiredService<ICryptoService>();
}

[Fact]
public async Task Get_ReturnsNotFound_WhenUserDoesNotExist()
{
var request = CreateRequest(HttpMethod.Get, Guid.NewGuid());

var response = await _client.SendAsync(request);

Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}

[Fact]
public async Task Get_ReturnsDecryptedKey_WhenUserExistsInDatabase()
{
var userId = Guid.NewGuid();
var plainKey = GenerateBase64Key("direct-insert-key");
var encryptedKey = await _cryptoService.AesEncryptToB64Async(plainKey);
await _userKeyRepository.CreateAsync(new UserKeyModel { Id = userId, Key = encryptedKey });

var request = CreateRequest(HttpMethod.Get, userId);
var response = await _client.SendAsync(request);

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<UserKeyResponseModel>();
Assert.Equal(plainKey, result.Key);
}

[Fact]
public async Task Post_ThenGet_RoundTripsKey()
{
var userId = Guid.NewGuid();
var key = GenerateBase64Key("test-key-value");
var beforePost = DateTime.UtcNow;

var postRequest = CreateRequest(HttpMethod.Post, userId, new { Key = key });
var postResponse = await _client.SendAsync(postRequest);
Assert.Equal(HttpStatusCode.OK, postResponse.StatusCode);

var getRequest = CreateRequest(HttpMethod.Get, userId);
var getResponse = await _client.SendAsync(getRequest);
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);

var result = await getResponse.Content.ReadFromJsonAsync<UserKeyResponseModel>();
Assert.Equal(key, result.Key);

var stored = await _userKeyRepository.ReadAsync(userId);
Assert.InRange(stored.CreationDate, beforePost, DateTime.UtcNow);
}

[Fact]
public async Task Post_ReturnsBadRequest_WhenUserAlreadyExists()
{
var userId = Guid.NewGuid();

var postRequest = CreateRequest(HttpMethod.Post, userId, new { Key = GenerateBase64Key("key1") });
await _client.SendAsync(postRequest);

var duplicateRequest = CreateRequest(HttpMethod.Post, userId, new { Key = GenerateBase64Key("key2") });
var response = await _client.SendAsync(duplicateRequest);

Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}

[Fact]
public async Task Put_CreatesUser_WhenUserDoesNotExist()
{
var userId = Guid.NewGuid();
var key = GenerateBase64Key("put-create-key");
var beforePut = DateTime.UtcNow;

var putRequest = CreateRequest(HttpMethod.Put, userId, new { Key = key });
var putResponse = await _client.SendAsync(putRequest);
Assert.Equal(HttpStatusCode.OK, putResponse.StatusCode);

var getRequest = CreateRequest(HttpMethod.Get, userId);
var getResponse = await _client.SendAsync(getRequest);
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);

var result = await getResponse.Content.ReadFromJsonAsync<UserKeyResponseModel>();
Assert.Equal(key, result.Key);

var stored = await _userKeyRepository.ReadAsync(userId);
Assert.InRange(stored.CreationDate, beforePut, DateTime.UtcNow);
}

[Fact]
public async Task Put_UpdatesKey_ThenGetReturnsUpdatedKey()
{
var userId = Guid.NewGuid();

var originalKey = GenerateBase64Key("original-key");
var updatedKey = GenerateBase64Key("updated-key");

var postRequest = CreateRequest(HttpMethod.Post, userId, new { Key = originalKey });
await _client.SendAsync(postRequest);

var beforePut = DateTime.UtcNow;
var putRequest = CreateRequest(HttpMethod.Put, userId, new { Key = updatedKey });
var putResponse = await _client.SendAsync(putRequest);
Assert.Equal(HttpStatusCode.OK, putResponse.StatusCode);

var getRequest = CreateRequest(HttpMethod.Get, userId);
var getResponse = await _client.SendAsync(getRequest);
var result = await getResponse.Content.ReadFromJsonAsync<UserKeyResponseModel>();
Assert.Equal(updatedKey, result.Key);

var stored = await _userKeyRepository.ReadAsync(userId);
Assert.NotNull(stored.RevisionDate);
Assert.InRange(stored.RevisionDate.Value, beforePut, DateTime.UtcNow);
}

private static string GenerateBase64Key(string label) =>
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(label));

private HttpRequestMessage CreateRequest(HttpMethod method, Guid userId, object body = null)
{
var request = new HttpRequestMessage(method, "/user-keys");
request.Headers.Add(TestAuthHandler.TestUserIdHeader, userId.ToString());
if (body != null)
{
request.Content = JsonContent.Create(body);
}
return request;
}
}
161 changes: 161 additions & 0 deletions test/KeyConnector.Tests/Controllers/UserKeysControllerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
ο»Ώusing System;
using System.Security.Claims;
using System.Threading.Tasks;
using Bit.KeyConnector.Controllers;
using Bit.KeyConnector.Models;
using Bit.KeyConnector.Repositories;
using Bit.KeyConnector.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;
using Xunit;

namespace KeyConnector.Tests.Controllers;

public class UserKeysControllerTests
{
private readonly Guid _userId = Guid.NewGuid();
private readonly IUserKeyRepository _userKeyRepository = Substitute.For<IUserKeyRepository>();
private readonly ICryptoService _cryptoService = Substitute.For<ICryptoService>();
private readonly UserKeysController _sut;

public UserKeysControllerTests()
{
var identityOptions = new IdentityOptions();
var logger = Substitute.For<ILogger<UserKeysController>>();

_sut = new UserKeysController(
Options.Create(identityOptions),
logger,
_userKeyRepository,
_cryptoService);

var claims = new ClaimsPrincipal(new ClaimsIdentity(
[
new Claim(identityOptions.ClaimsIdentity.UserIdClaimType, _userId.ToString())
]));
_sut.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext { User = claims }
};
}

[Fact]
public async Task Get_ReturnsNotFound_WhenUserDoesNotExist()
{
_userKeyRepository.ReadAsync(_userId).Returns((UserKeyModel)null);

var result = await _sut.Get();

Assert.IsType<NotFoundResult>(result);
}

[Fact]
public async Task Get_ReturnsDecryptedKey_WhenUserExists()
{
var user = new UserKeyModel { Id = _userId, Key = "encryptedKey" };
_userKeyRepository.ReadAsync(_userId).Returns(user);
_cryptoService.AesDecryptToB64Async("encryptedKey").Returns("decryptedKey");

var result = await _sut.Get();

var jsonResult = Assert.IsType<JsonResult>(result);
var response = Assert.IsType<UserKeyResponseModel>(jsonResult.Value);
Assert.Equal("decryptedKey", response.Key);
}

[Fact]
public async Task Get_UpdatesLastAccessDate_WhenUserExists()
{
var user = new UserKeyModel { Id = _userId, Key = "encryptedKey" };
_userKeyRepository.ReadAsync(_userId).Returns(user);
_cryptoService.AesDecryptToB64Async("encryptedKey").Returns("decryptedKey");
var beforeGet = DateTime.UtcNow;

await _sut.Get();

Assert.NotNull(user.LastAccessDate);
Assert.InRange(user.LastAccessDate.Value, beforeGet, DateTime.UtcNow);
await _userKeyRepository.Received(1).UpdateAsync(user);
}

[Fact]
public async Task Post_ReturnsBadRequest_WhenUserAlreadyExists()
{
var existingUser = new UserKeyModel { Id = _userId, Key = "existingKey" };
_userKeyRepository.ReadAsync(_userId).Returns(existingUser);

var result = await _sut.Post(new UserKeyRequestModel { Key = "newKey" });

Assert.IsType<BadRequestResult>(result);
await _userKeyRepository.DidNotReceive().CreateAsync(Arg.Any<UserKeyModel>());
}

[Fact]
public async Task Post_CreatesUser_WhenUserDoesNotExist()
{
_userKeyRepository.ReadAsync(_userId).Returns((UserKeyModel)null);
_cryptoService.AesEncryptToB64Async("plaintextKey").Returns("encryptedKey");
UserKeyModel capturedUser = null;
await _userKeyRepository.CreateAsync(Arg.Do<UserKeyModel>(u => capturedUser = u));
var beforeCreate = DateTime.UtcNow;

var result = await _sut.Post(new UserKeyRequestModel { Key = "plaintextKey" });

Assert.IsType<OkResult>(result);
Assert.NotNull(capturedUser);
Assert.Equal(_userId, capturedUser.Id);
Assert.Equal("encryptedKey", capturedUser.Key);
Assert.InRange(capturedUser.CreationDate, beforeCreate, DateTime.UtcNow);
await _userKeyRepository.Received(1).CreateAsync(capturedUser);
}

[Fact]
public async Task Put_CreatesUser_WhenUserDoesNotExist()
{
_userKeyRepository.ReadAsync(_userId).Returns((UserKeyModel)null);
_cryptoService.AesEncryptToB64Async("newKey").Returns("encryptedNewKey");
UserKeyModel capturedUser = null;
await _userKeyRepository.CreateAsync(Arg.Do<UserKeyModel>(u => capturedUser = u));
var beforeCreate = DateTime.UtcNow;

var result = await _sut.Put(new UserKeyRequestModel { Key = "newKey" });

Assert.IsType<OkResult>(result);
Assert.NotNull(capturedUser);
Assert.Equal(_userId, capturedUser.Id);
Assert.Equal("encryptedNewKey", capturedUser.Key);
Assert.InRange(capturedUser.CreationDate, beforeCreate, DateTime.UtcNow);
await _userKeyRepository.Received(1).CreateAsync(capturedUser);
await _userKeyRepository.DidNotReceive().UpdateAsync(Arg.Any<UserKeyModel>());
}

[Fact]
public async Task Put_UpdatesKey_WhenUserExists()
{
var creationDate = DateTime.UtcNow.AddDays(-1);
var existingUser = new UserKeyModel
{
Id = _userId,
Key = "oldEncryptedKey",
CreationDate = creationDate
};
_userKeyRepository.ReadAsync(_userId).Returns(existingUser);
_cryptoService.AesEncryptToB64Async("newPlaintextKey").Returns("newEncryptedKey");
var beforeUpdate = DateTime.UtcNow;

var result = await _sut.Put(new UserKeyRequestModel { Key = "newPlaintextKey" });

Assert.IsType<OkResult>(result);
Assert.Equal(_userId, existingUser.Id);
Assert.Equal("newEncryptedKey", existingUser.Key);
Assert.Equal(creationDate, existingUser.CreationDate);
Assert.NotNull(existingUser.RevisionDate);
Assert.InRange(existingUser.RevisionDate.Value, beforeUpdate, DateTime.UtcNow);
await _userKeyRepository.Received(1).UpdateAsync(existingUser);
await _userKeyRepository.DidNotReceive().CreateAsync(Arg.Any<UserKeyModel>());
}
}
Loading
Loading