Browse Source
* Add ability to fetch events by service account * Extract GetDateRange into ApiHelpers util * Add dapper implementation * Add EF repo implementation * Add authz handler case * unit + integration tests for controller * swap to read check * Adding comments * Fix integration tests from merge * Enabled SM events controller for self-hostingpull/3326/head
15 changed files with 461 additions and 28 deletions
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
using Bit.Api.Models.Response; |
||||
using Bit.Api.Utilities; |
||||
using Bit.Core.Exceptions; |
||||
using Bit.Core.Models.Data; |
||||
using Bit.Core.Repositories; |
||||
using Bit.Core.SecretsManager.AuthorizationRequirements; |
||||
using Bit.Core.SecretsManager.Repositories; |
||||
using Microsoft.AspNetCore.Authorization; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
|
||||
namespace Bit.Api.SecretsManager.Controllers; |
||||
|
||||
[Authorize("secrets")] |
||||
public class SecretsManagerEventsController : Controller |
||||
{ |
||||
private readonly IAuthorizationService _authorizationService; |
||||
private readonly IEventRepository _eventRepository; |
||||
private readonly IServiceAccountRepository _serviceAccountRepository; |
||||
|
||||
public SecretsManagerEventsController( |
||||
IEventRepository eventRepository, |
||||
IServiceAccountRepository serviceAccountRepository, |
||||
IAuthorizationService authorizationService) |
||||
{ |
||||
_authorizationService = authorizationService; |
||||
_serviceAccountRepository = serviceAccountRepository; |
||||
_eventRepository = eventRepository; |
||||
} |
||||
|
||||
[HttpGet("sm/events/service-accounts/{serviceAccountId}")] |
||||
public async Task<ListResponseModel<EventResponseModel>> GetServiceAccountEventsAsync(Guid serviceAccountId, |
||||
[FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, |
||||
[FromQuery] string continuationToken = null) |
||||
{ |
||||
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(serviceAccountId); |
||||
var authorizationResult = |
||||
await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.ReadEvents); |
||||
|
||||
if (!authorizationResult.Succeeded) |
||||
{ |
||||
throw new NotFoundException(); |
||||
} |
||||
|
||||
var dateRange = ApiHelpers.GetDateRange(start, end); |
||||
|
||||
var result = await _eventRepository.GetManyByOrganizationServiceAccountAsync(serviceAccount.OrganizationId, |
||||
serviceAccount.Id, dateRange.Item1, dateRange.Item2, |
||||
new PageOptions { ContinuationToken = continuationToken }); |
||||
var responses = result.Data.Select(e => new EventResponseModel(e)); |
||||
return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken); |
||||
} |
||||
} |
||||
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
using Bit.Core.Models.Data; |
||||
using Bit.Infrastructure.EntityFramework.Models; |
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries; |
||||
|
||||
public class EventReadPageByOrganizationIdServiceAccountIdQuery : IQuery<Event> |
||||
{ |
||||
private readonly Guid _organizationId; |
||||
private readonly Guid _serviceAccountId; |
||||
private readonly DateTime _startDate; |
||||
private readonly DateTime _endDate; |
||||
private readonly DateTime? _beforeDate; |
||||
private readonly PageOptions _pageOptions; |
||||
|
||||
public EventReadPageByOrganizationIdServiceAccountIdQuery(Guid organizationId, Guid serviceAccountId, |
||||
DateTime startDate, DateTime endDate, DateTime? beforeDate, PageOptions pageOptions) |
||||
{ |
||||
_organizationId = organizationId; |
||||
_serviceAccountId = serviceAccountId; |
||||
_startDate = startDate; |
||||
_endDate = endDate; |
||||
_beforeDate = beforeDate; |
||||
_pageOptions = pageOptions; |
||||
} |
||||
|
||||
public IQueryable<Event> Run(DatabaseContext dbContext) |
||||
{ |
||||
var q = from e in dbContext.Events |
||||
where e.Date >= _startDate && |
||||
(_beforeDate != null || e.Date <= _endDate) && |
||||
(_beforeDate == null || e.Date < _beforeDate.Value) && |
||||
e.OrganizationId == _organizationId && |
||||
e.ServiceAccountId == _serviceAccountId |
||||
orderby e.Date descending |
||||
select e; |
||||
return q.Skip(0).Take(_pageOptions.PageSize); |
||||
} |
||||
} |
||||
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
CREATE PROCEDURE [dbo].[Event_ReadPageByOrganizationIdServiceAccountId] |
||||
@OrganizationId UNIQUEIDENTIFIER, |
||||
@ServiceAccountId UNIQUEIDENTIFIER, |
||||
@StartDate DATETIME2(7), |
||||
@EndDate DATETIME2(7), |
||||
@BeforeDate DATETIME2(7), |
||||
@PageSize INT |
||||
AS |
||||
BEGIN |
||||
SET NOCOUNT ON |
||||
|
||||
SELECT |
||||
* |
||||
FROM |
||||
[dbo].[EventView] |
||||
WHERE |
||||
[Date] >= @StartDate |
||||
AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate) |
||||
AND (@BeforeDate IS NULL OR [Date] < @BeforeDate) |
||||
AND [OrganizationId] = @OrganizationId |
||||
AND [ServiceAccountId] = @ServiceAccountId |
||||
ORDER BY [Date] DESC |
||||
OFFSET 0 ROWS |
||||
FETCH NEXT @PageSize ROWS ONLY |
||||
END |
||||
@ -0,0 +1,71 @@
@@ -0,0 +1,71 @@
|
||||
using System.Net; |
||||
using System.Net.Http.Headers; |
||||
using Bit.Api.IntegrationTest.Factories; |
||||
using Bit.Core.SecretsManager.Entities; |
||||
using Bit.Core.SecretsManager.Repositories; |
||||
using Xunit; |
||||
|
||||
namespace Bit.Api.IntegrationTest.SecretsManager.Controllers; |
||||
|
||||
public class SecretsManagerEventsControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime |
||||
{ |
||||
private const string _mockEncryptedString = |
||||
"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; |
||||
|
||||
private readonly HttpClient _client; |
||||
private readonly ApiApplicationFactory _factory; |
||||
|
||||
private readonly IServiceAccountRepository _serviceAccountRepository; |
||||
|
||||
private string _email = null!; |
||||
private SecretsManagerOrganizationHelper _organizationHelper = null!; |
||||
|
||||
public SecretsManagerEventsControllerTests(ApiApplicationFactory factory) |
||||
{ |
||||
_factory = factory; |
||||
_client = _factory.CreateClient(); |
||||
_serviceAccountRepository = _factory.GetService<IServiceAccountRepository>(); |
||||
} |
||||
|
||||
public async Task InitializeAsync() |
||||
{ |
||||
_email = $"integration-test{Guid.NewGuid()}@bitwarden.com"; |
||||
await _factory.LoginWithNewAccount(_email); |
||||
_organizationHelper = new SecretsManagerOrganizationHelper(_factory, _email); |
||||
} |
||||
|
||||
public Task DisposeAsync() |
||||
{ |
||||
_client.Dispose(); |
||||
return Task.CompletedTask; |
||||
} |
||||
|
||||
private async Task LoginAsync(string email) |
||||
{ |
||||
var tokens = await _factory.LoginAsync(email); |
||||
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); |
||||
} |
||||
|
||||
[Theory] |
||||
[InlineData(false, false, false)] |
||||
[InlineData(false, false, true)] |
||||
[InlineData(false, true, false)] |
||||
[InlineData(false, true, true)] |
||||
[InlineData(true, false, false)] |
||||
[InlineData(true, false, true)] |
||||
[InlineData(true, true, false)] |
||||
public async Task GetServiceAccountEvents_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled) |
||||
{ |
||||
var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled); |
||||
await LoginAsync(_email); |
||||
|
||||
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount |
||||
{ |
||||
OrganizationId = org.Id, |
||||
Name = _mockEncryptedString |
||||
}); |
||||
|
||||
var response = await _client.GetAsync($"/sm/events/service-accounts/{serviceAccount.Id}"); |
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); |
||||
} |
||||
} |
||||
@ -0,0 +1,79 @@
@@ -0,0 +1,79 @@
|
||||
using System.Security.Claims; |
||||
using Bit.Api.SecretsManager.Controllers; |
||||
using Bit.Core.Exceptions; |
||||
using Bit.Core.Models.Data; |
||||
using Bit.Core.Repositories; |
||||
using Bit.Core.SecretsManager.Entities; |
||||
using Bit.Core.SecretsManager.Repositories; |
||||
using Bit.Test.Common.AutoFixture; |
||||
using Bit.Test.Common.AutoFixture.Attributes; |
||||
using Microsoft.AspNetCore.Authorization; |
||||
using NSubstitute; |
||||
using Xunit; |
||||
|
||||
namespace Bit.Api.Test.SecretsManager.Controllers; |
||||
|
||||
[ControllerCustomize(typeof(SecretsManagerEventsController))] |
||||
[SutProviderCustomize] |
||||
[JsonDocumentCustomize] |
||||
public class SecretsManagerEventsControllerTests |
||||
{ |
||||
[Theory] |
||||
[BitAutoData] |
||||
public async void GetServiceAccountEvents_NoAccess_Throws(SutProvider<SecretsManagerEventsController> sutProvider, |
||||
ServiceAccount data) |
||||
{ |
||||
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsForAnyArgs(data); |
||||
sutProvider.GetDependency<IAuthorizationService>() |
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data, |
||||
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed()); |
||||
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetServiceAccountEventsAsync(data.Id)); |
||||
await sutProvider.GetDependency<IEventRepository>().DidNotReceiveWithAnyArgs() |
||||
.GetManyByOrganizationServiceAccountAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<DateTime>(), |
||||
Arg.Any<DateTime>(), Arg.Any<PageOptions>()); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async void GetServiceAccountEvents_DateRangeOver_Throws( |
||||
SutProvider<SecretsManagerEventsController> sutProvider, |
||||
ServiceAccount data) |
||||
{ |
||||
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsForAnyArgs(data); |
||||
sutProvider.GetDependency<IAuthorizationService>() |
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data, |
||||
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success()); |
||||
|
||||
var start = DateTime.UtcNow.AddYears(-1); |
||||
var end = DateTime.UtcNow.AddYears(1); |
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => |
||||
sutProvider.Sut.GetServiceAccountEventsAsync(data.Id, start, end)); |
||||
|
||||
await sutProvider.GetDependency<IEventRepository>().DidNotReceiveWithAnyArgs() |
||||
.GetManyByOrganizationServiceAccountAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<DateTime>(), |
||||
Arg.Any<DateTime>(), Arg.Any<PageOptions>()); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async void GetServiceAccountEvents_Success(SutProvider<SecretsManagerEventsController> sutProvider, |
||||
ServiceAccount data) |
||||
{ |
||||
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsForAnyArgs(data); |
||||
sutProvider.GetDependency<IAuthorizationService>() |
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data, |
||||
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success()); |
||||
sutProvider.GetDependency<IEventRepository>() |
||||
.GetManyByOrganizationServiceAccountAsync(default, default, default, default, default) |
||||
.ReturnsForAnyArgs(new PagedResult<IEvent>()); |
||||
|
||||
await sutProvider.Sut.GetServiceAccountEventsAsync(data.Id); |
||||
|
||||
await sutProvider.GetDependency<IEventRepository>().Received(1) |
||||
.GetManyByOrganizationServiceAccountAsync(data.OrganizationId, data.Id, Arg.Any<DateTime>(), |
||||
Arg.Any<DateTime>(), Arg.Any<PageOptions>()); |
||||
} |
||||
} |
||||
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Event_ReadPageByOrganizationIdServiceAccountId] |
||||
@OrganizationId UNIQUEIDENTIFIER, |
||||
@ServiceAccountId UNIQUEIDENTIFIER, |
||||
@StartDate DATETIME2(7), |
||||
@EndDate DATETIME2(7), |
||||
@BeforeDate DATETIME2(7), |
||||
@PageSize INT |
||||
AS |
||||
BEGIN |
||||
SET NOCOUNT ON |
||||
|
||||
SELECT |
||||
* |
||||
FROM |
||||
[dbo].[EventView] |
||||
WHERE |
||||
[Date] >= @StartDate |
||||
AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate) |
||||
AND (@BeforeDate IS NULL OR [Date] < @BeforeDate) |
||||
AND [OrganizationId] = @OrganizationId |
||||
AND [ServiceAccountId] = @ServiceAccountId |
||||
ORDER BY [Date] DESC |
||||
OFFSET 0 ROWS |
||||
FETCH NEXT @PageSize ROWS ONLY |
||||
END |
||||
Loading…
Reference in new issue