mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[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
This commit is contained in:
parent
c1cf07d764
commit
728cd1c0b5
@ -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
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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) };
|
||||
}
|
||||
|
@ -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
|
||||
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
|
||||
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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 @@
|
||||
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 @@
|
||||
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 @@
|
||||
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 @@
|
||||
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
Block a user