Browse Source

[SM-949] Add endpoint to fetch events by service account (#3336)

* 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-hosting
pull/3326/head
Thomas Avery 2 years ago committed by GitHub
parent
commit
728cd1c0b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/ServiceAccounts/ServiceAccountAuthorizationHandler.cs
  2. 59
      bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/ServiceAccounts/ServiceAccountAuthorizationHandlerTests.cs
  3. 35
      src/Api/Controllers/EventsController.cs
  4. 52
      src/Api/SecretsManager/Controllers/SecretsManagerEventsController.cs
  5. 32
      src/Api/Utilities/ApiHelpers.cs
  6. 2
      src/Core/Repositories/IEventRepository.cs
  7. 8
      src/Core/Repositories/TableStorage/EventRepository.cs
  8. 1
      src/Core/SecretsManager/AuthorizationRequirements/ServiceAccountOperationRequirement.cs
  9. 18
      src/Infrastructure.Dapper/Repositories/EventRepository.cs
  10. 26
      src/Infrastructure.EntityFramework/Repositories/EventRepository.cs
  11. 38
      src/Infrastructure.EntityFramework/Repositories/Queries/EventReadPageByOrganizationIdServiceAccountIdQuery.cs
  12. 25
      src/Sql/SecretsManager/dbo/Stored Procedures/Event/Event_ReadPageByOrganizationIdServiceAccountId.sql
  13. 71
      test/Api.IntegrationTest/SecretsManager/Controllers/SecretsManagerEventsControllerTests.cs
  14. 79
      test/Api.Test/SecretsManager/Controllers/SecretsManagerEventsControllerTests.cs
  15. 25
      util/Migrator/DbScripts/2023-10-09_00_Event_ReadPageByOrganizationIdServiceAccountId.sql

18
bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/ServiceAccounts/ServiceAccountAuthorizationHandler.cs

@ -56,6 +56,9 @@ public class @@ -56,6 +56,9 @@ public class
case not null when requirement == ServiceAccountOperations.RevokeAccessTokens:
await CanRevokeAccessTokensAsync(context, requirement, resource);
break;
case not null when requirement == ServiceAccountOperations.ReadEvents:
await CanReadEventsAsync(context, requirement, resource);
break;
default:
throw new ArgumentException("Unsupported operation requirement type provided.",
nameof(requirement));
@ -169,4 +172,19 @@ public class @@ -169,4 +172,19 @@ public class
context.Succeed(requirement);
}
}
private async Task CanReadEventsAsync(AuthorizationHandlerContext context,
ServiceAccountOperationRequirement requirement, ServiceAccount resource)
{
var (accessClient, userId) =
await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);
var access =
await _serviceAccountRepository.AccessToServiceAccountAsync(resource.Id, userId,
accessClient);
if (access.Read)
{
context.Succeed(requirement);
}
}
}

59
bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/ServiceAccounts/ServiceAccountAuthorizationHandlerTests.cs

@ -497,4 +497,63 @@ public class ServiceAccountAuthorizationHandlerTests @@ -497,4 +497,63 @@ public class ServiceAccountAuthorizationHandlerTests
Assert.Equal(expected, authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task CanReadEvents_AccessToSecretsManagerFalse_DoesNotSucceed(
SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountOperations.ReadEvents;
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId)
.Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, serviceAccount);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task CanReadEvents_NullResource_DoesNotSucceed(
SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,
ClaimsPrincipal claimsPrincipal,
Guid userId)
{
var requirement = ServiceAccountOperations.ReadEvents;
SetupPermission(sutProvider, PermissionType.RunAsAdmin, serviceAccount.OrganizationId, userId);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, null);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false, true)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true)]
public async Task CanReadEvents_AccessCheck(PermissionType permissionType, bool read, bool write,
bool expected,
SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,
ClaimsPrincipal claimsPrincipal,
Guid userId)
{
var requirement = ServiceAccountOperations.ReadEvents;
SetupPermission(sutProvider, permissionType, serviceAccount.OrganizationId, userId);
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountAsync(serviceAccount.Id, userId, Arg.Any<AccessClientType>())
.Returns((read, write));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, serviceAccount);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.Equal(expected, authzContext.HasSucceeded);
}
}

35
src/Api/Controllers/EventsController.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using Bit.Api.Models.Response;
using Bit.Api.Utilities;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
@ -41,7 +42,7 @@ public class EventsController : Controller @@ -41,7 +42,7 @@ public class EventsController : Controller
public async Task<ListResponseModel<EventResponseModel>> GetUser(
[FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, [FromQuery] string continuationToken = null)
{
var dateRange = GetDateRange(start, end);
var dateRange = ApiHelpers.GetDateRange(start, end);
var userId = _userService.GetProperUserId(User).Value;
var result = await _eventRepository.GetManyByUserAsync(userId, dateRange.Item1, dateRange.Item2,
new PageOptions { ContinuationToken = continuationToken });
@ -75,7 +76,7 @@ public class EventsController : Controller @@ -75,7 +76,7 @@ public class EventsController : Controller
throw new NotFoundException();
}
var dateRange = GetDateRange(start, end);
var dateRange = ApiHelpers.GetDateRange(start, end);
var result = await _eventRepository.GetManyByCipherAsync(cipher, dateRange.Item1, dateRange.Item2,
new PageOptions { ContinuationToken = continuationToken });
var responses = result.Data.Select(e => new EventResponseModel(e));
@ -92,7 +93,7 @@ public class EventsController : Controller @@ -92,7 +93,7 @@ public class EventsController : Controller
throw new NotFoundException();
}
var dateRange = GetDateRange(start, end);
var dateRange = ApiHelpers.GetDateRange(start, end);
var result = await _eventRepository.GetManyByOrganizationAsync(orgId, dateRange.Item1, dateRange.Item2,
new PageOptions { ContinuationToken = continuationToken });
var responses = result.Data.Select(e => new EventResponseModel(e));
@ -110,7 +111,7 @@ public class EventsController : Controller @@ -110,7 +111,7 @@ public class EventsController : Controller
throw new NotFoundException();
}
var dateRange = GetDateRange(start, end);
var dateRange = ApiHelpers.GetDateRange(start, end);
var result = await _eventRepository.GetManyByOrganizationActingUserAsync(organizationUser.OrganizationId,
organizationUser.UserId.Value, dateRange.Item1, dateRange.Item2,
new PageOptions { ContinuationToken = continuationToken });
@ -127,7 +128,7 @@ public class EventsController : Controller @@ -127,7 +128,7 @@ public class EventsController : Controller
throw new NotFoundException();
}
var dateRange = GetDateRange(start, end);
var dateRange = ApiHelpers.GetDateRange(start, end);
var result = await _eventRepository.GetManyByProviderAsync(providerId, dateRange.Item1, dateRange.Item2,
new PageOptions { ContinuationToken = continuationToken });
var responses = result.Data.Select(e => new EventResponseModel(e));
@ -145,33 +146,11 @@ public class EventsController : Controller @@ -145,33 +146,11 @@ public class EventsController : Controller
throw new NotFoundException();
}
var dateRange = GetDateRange(start, end);
var dateRange = ApiHelpers.GetDateRange(start, end);
var result = await _eventRepository.GetManyByProviderActingUserAsync(providerUser.ProviderId,
providerUser.UserId.Value, dateRange.Item1, dateRange.Item2,
new PageOptions { ContinuationToken = continuationToken });
var responses = result.Data.Select(e => new EventResponseModel(e));
return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);
}
private Tuple<DateTime, DateTime> GetDateRange(DateTime? start, DateTime? end)
{
if (!end.HasValue || !start.HasValue)
{
end = DateTime.UtcNow.Date.AddDays(1).AddMilliseconds(-1);
start = DateTime.UtcNow.Date.AddDays(-30);
}
else if (start.Value > end.Value)
{
var newEnd = start;
start = end;
end = newEnd;
}
if ((end.Value - start.Value) > TimeSpan.FromDays(367))
{
throw new BadRequestException("Range too large.");
}
return new Tuple<DateTime, DateTime>(start.Value, end.Value);
}
}

52
src/Api/SecretsManager/Controllers/SecretsManagerEventsController.cs

@ -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);
}
}

32
src/Api/Utilities/ApiHelpers.cs

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
using System.Text.Json;
using Azure.Messaging.EventGrid;
using Azure.Messaging.EventGrid.SystemEvents;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Mvc;
@ -69,4 +70,35 @@ public static class ApiHelpers @@ -69,4 +70,35 @@ public static class ApiHelpers
return new OkObjectResult(response);
}
/// <summary>
/// Validates and returns a date range. Currently used for fetching events.
/// </summary>
/// <param name="start">start date and time</param>
/// <param name="end">end date and time</param>
/// <remarks>
/// If start or end are null, will return a range of the last 30 days.
/// If a time span greater than 367 days is passed will throw BadRequestException.
/// </remarks>
public static Tuple<DateTime, DateTime> GetDateRange(DateTime? start, DateTime? end)
{
if (!end.HasValue || !start.HasValue)
{
end = DateTime.UtcNow.Date.AddDays(1).AddMilliseconds(-1);
start = DateTime.UtcNow.Date.AddDays(-30);
}
else if (start.Value > end.Value)
{
var newEnd = start;
start = end;
end = newEnd;
}
if ((end.Value - start.Value) > TimeSpan.FromDays(367))
{
throw new BadRequestException("Range too large.");
}
return new Tuple<DateTime, DateTime>(start.Value, end.Value);
}
}

2
src/Core/Repositories/IEventRepository.cs

@ -19,4 +19,6 @@ public interface IEventRepository @@ -19,4 +19,6 @@ public interface IEventRepository
PageOptions pageOptions);
Task CreateAsync(IEvent e);
Task CreateManyAsync(IEnumerable<IEvent> e);
Task<PagedResult<IEvent>> GetManyByOrganizationServiceAccountAsync(Guid organizationId, Guid serviceAccountId,
DateTime startDate, DateTime endDate, PageOptions pageOptions);
}

8
src/Core/Repositories/TableStorage/EventRepository.cs

@ -61,6 +61,14 @@ public class EventRepository : IEventRepository @@ -61,6 +61,14 @@ public class EventRepository : IEventRepository
return await GetManyAsync(partitionKey, $"CipherId={cipher.Id}__Date={{0}}", startDate, endDate, pageOptions);
}
public async Task<PagedResult<IEvent>> GetManyByOrganizationServiceAccountAsync(Guid organizationId,
Guid serviceAccountId, DateTime startDate, DateTime endDate, PageOptions pageOptions)
{
return await GetManyAsync($"OrganizationId={organizationId}",
$"ServiceAccountId={serviceAccountId}__Date={{0}}", startDate, endDate, pageOptions);
}
public async Task CreateAsync(IEvent e)
{
if (!(e is EventTableEntity entity))

1
src/Core/SecretsManager/AuthorizationRequirements/ServiceAccountOperationRequirement.cs

@ -15,4 +15,5 @@ public static class ServiceAccountOperations @@ -15,4 +15,5 @@ public static class ServiceAccountOperations
public static readonly ServiceAccountOperationRequirement ReadAccessTokens = new() { Name = nameof(ReadAccessTokens) };
public static readonly ServiceAccountOperationRequirement CreateAccessToken = new() { Name = nameof(CreateAccessToken) };
public static readonly ServiceAccountOperationRequirement RevokeAccessTokens = new() { Name = nameof(RevokeAccessTokens) };
public static readonly ServiceAccountOperationRequirement ReadEvents = new() { Name = nameof(ReadEvents) };
}

18
src/Infrastructure.Dapper/Repositories/EventRepository.cs

@ -118,6 +118,18 @@ public class EventRepository : Repository<Event, Guid>, IEventRepository @@ -118,6 +118,18 @@ public class EventRepository : Repository<Event, Guid>, IEventRepository
}
}
public async Task<PagedResult<IEvent>> GetManyByOrganizationServiceAccountAsync(Guid organizationId, Guid serviceAccountId,
DateTime startDate, DateTime endDate,
PageOptions pageOptions)
{
return await GetManyAsync($"[{Schema}].[Event_ReadPageByOrganizationIdServiceAccountId]",
new Dictionary<string, object>
{
["@OrganizationId"] = organizationId,
["@ServiceAccountId"] = serviceAccountId
}, startDate, endDate, pageOptions);
}
private async Task<PagedResult<IEvent>> GetManyAsync(string sprocName,
IDictionary<string, object> sprocParams, DateTime startDate, DateTime endDate, PageOptions pageOptions)
{
@ -187,6 +199,10 @@ public class EventRepository : Repository<Event, Guid>, IEventRepository @@ -187,6 +199,10 @@ public class EventRepository : Repository<Event, Guid>, IEventRepository
eventsTable.Columns.Add(ipAddressColumn);
var dateColumn = new DataColumn(nameof(e.Date), typeof(DateTime));
eventsTable.Columns.Add(dateColumn);
var secretIdColumn = new DataColumn(nameof(e.SecretId), typeof(Guid));
eventsTable.Columns.Add(secretIdColumn);
var serviceAccountIdColumn = new DataColumn(nameof(e.ServiceAccountId), typeof(Guid));
eventsTable.Columns.Add(serviceAccountIdColumn);
foreach (DataColumn col in eventsTable.Columns)
{
@ -217,6 +233,8 @@ public class EventRepository : Repository<Event, Guid>, IEventRepository @@ -217,6 +233,8 @@ public class EventRepository : Repository<Event, Guid>, IEventRepository
row[deviceTypeColumn] = ev.DeviceType.HasValue ? (object)ev.DeviceType.Value : DBNull.Value;
row[ipAddressColumn] = ev.IpAddress != null ? (object)ev.IpAddress : DBNull.Value;
row[dateColumn] = ev.Date;
row[secretIdColumn] = ev.SecretId.HasValue ? ev.SecretId.Value : DBNull.Value;
row[serviceAccountIdColumn] = ev.ServiceAccountId.HasValue ? ev.ServiceAccountId.Value : DBNull.Value;
eventsTable.Rows.Add(row);
}

26
src/Infrastructure.EntityFramework/Repositories/EventRepository.cs

@ -49,6 +49,32 @@ public class EventRepository : Repository<Core.Entities.Event, Event, Guid>, IEv @@ -49,6 +49,32 @@ public class EventRepository : Repository<Core.Entities.Event, Event, Guid>, IEv
}
}
public async Task<PagedResult<IEvent>> GetManyByOrganizationServiceAccountAsync(Guid organizationId, Guid serviceAccountId,
DateTime startDate, DateTime endDate,
PageOptions pageOptions)
{
DateTime? beforeDate = null;
if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) &&
long.TryParse(pageOptions.ContinuationToken, out var binaryDate))
{
beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc);
}
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var query = new EventReadPageByOrganizationIdServiceAccountIdQuery(organizationId, serviceAccountId,
startDate, endDate, beforeDate, pageOptions);
var events = await query.Run(dbContext).ToListAsync();
var result = new PagedResult<IEvent>();
if (events.Any() && events.Count >= pageOptions.PageSize)
{
result.ContinuationToken = events.Last().Date.ToBinary().ToString();
}
result.Data.AddRange(events);
return result;
}
public async Task<PagedResult<IEvent>> GetManyByCipherAsync(Cipher cipher, DateTime startDate, DateTime endDate, PageOptions pageOptions)
{
DateTime? beforeDate = null;

38
src/Infrastructure.EntityFramework/Repositories/Queries/EventReadPageByOrganizationIdServiceAccountIdQuery.cs

@ -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);
}
}

25
src/Sql/SecretsManager/dbo/Stored Procedures/Event/Event_ReadPageByOrganizationIdServiceAccountId.sql

@ -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

71
test/Api.IntegrationTest/SecretsManager/Controllers/SecretsManagerEventsControllerTests.cs

@ -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);
}
}

79
test/Api.Test/SecretsManager/Controllers/SecretsManagerEventsControllerTests.cs

@ -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>());
}
}

25
util/Migrator/DbScripts/2023-10-09_00_Event_ReadPageByOrganizationIdServiceAccountId.sql

@ -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…
Cancel
Save