mirror of
https://github.com/bitwarden/server.git
synced 2024-11-25 12:45:18 +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:
|
case not null when requirement == ServiceAccountOperations.RevokeAccessTokens:
|
||||||
await CanRevokeAccessTokensAsync(context, requirement, resource);
|
await CanRevokeAccessTokensAsync(context, requirement, resource);
|
||||||
break;
|
break;
|
||||||
|
case not null when requirement == ServiceAccountOperations.ReadEvents:
|
||||||
|
await CanReadEventsAsync(context, requirement, resource);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentException("Unsupported operation requirement type provided.",
|
throw new ArgumentException("Unsupported operation requirement type provided.",
|
||||||
nameof(requirement));
|
nameof(requirement));
|
||||||
@ -169,4 +172,19 @@ public class
|
|||||||
context.Succeed(requirement);
|
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);
|
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.Models.Response;
|
||||||
|
using Bit.Api.Utilities;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
@ -41,7 +42,7 @@ public class EventsController : Controller
|
|||||||
public async Task<ListResponseModel<EventResponseModel>> GetUser(
|
public async Task<ListResponseModel<EventResponseModel>> GetUser(
|
||||||
[FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, [FromQuery] string continuationToken = null)
|
[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 userId = _userService.GetProperUserId(User).Value;
|
||||||
var result = await _eventRepository.GetManyByUserAsync(userId, dateRange.Item1, dateRange.Item2,
|
var result = await _eventRepository.GetManyByUserAsync(userId, dateRange.Item1, dateRange.Item2,
|
||||||
new PageOptions { ContinuationToken = continuationToken });
|
new PageOptions { ContinuationToken = continuationToken });
|
||||||
@ -75,7 +76,7 @@ public class EventsController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var dateRange = GetDateRange(start, end);
|
var dateRange = ApiHelpers.GetDateRange(start, end);
|
||||||
var result = await _eventRepository.GetManyByCipherAsync(cipher, dateRange.Item1, dateRange.Item2,
|
var result = await _eventRepository.GetManyByCipherAsync(cipher, dateRange.Item1, dateRange.Item2,
|
||||||
new PageOptions { ContinuationToken = continuationToken });
|
new PageOptions { ContinuationToken = continuationToken });
|
||||||
var responses = result.Data.Select(e => new EventResponseModel(e));
|
var responses = result.Data.Select(e => new EventResponseModel(e));
|
||||||
@ -92,7 +93,7 @@ public class EventsController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var dateRange = GetDateRange(start, end);
|
var dateRange = ApiHelpers.GetDateRange(start, end);
|
||||||
var result = await _eventRepository.GetManyByOrganizationAsync(orgId, dateRange.Item1, dateRange.Item2,
|
var result = await _eventRepository.GetManyByOrganizationAsync(orgId, dateRange.Item1, dateRange.Item2,
|
||||||
new PageOptions { ContinuationToken = continuationToken });
|
new PageOptions { ContinuationToken = continuationToken });
|
||||||
var responses = result.Data.Select(e => new EventResponseModel(e));
|
var responses = result.Data.Select(e => new EventResponseModel(e));
|
||||||
@ -110,7 +111,7 @@ public class EventsController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var dateRange = GetDateRange(start, end);
|
var dateRange = ApiHelpers.GetDateRange(start, end);
|
||||||
var result = await _eventRepository.GetManyByOrganizationActingUserAsync(organizationUser.OrganizationId,
|
var result = await _eventRepository.GetManyByOrganizationActingUserAsync(organizationUser.OrganizationId,
|
||||||
organizationUser.UserId.Value, dateRange.Item1, dateRange.Item2,
|
organizationUser.UserId.Value, dateRange.Item1, dateRange.Item2,
|
||||||
new PageOptions { ContinuationToken = continuationToken });
|
new PageOptions { ContinuationToken = continuationToken });
|
||||||
@ -127,7 +128,7 @@ public class EventsController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var dateRange = GetDateRange(start, end);
|
var dateRange = ApiHelpers.GetDateRange(start, end);
|
||||||
var result = await _eventRepository.GetManyByProviderAsync(providerId, dateRange.Item1, dateRange.Item2,
|
var result = await _eventRepository.GetManyByProviderAsync(providerId, dateRange.Item1, dateRange.Item2,
|
||||||
new PageOptions { ContinuationToken = continuationToken });
|
new PageOptions { ContinuationToken = continuationToken });
|
||||||
var responses = result.Data.Select(e => new EventResponseModel(e));
|
var responses = result.Data.Select(e => new EventResponseModel(e));
|
||||||
@ -145,33 +146,11 @@ public class EventsController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var dateRange = GetDateRange(start, end);
|
var dateRange = ApiHelpers.GetDateRange(start, end);
|
||||||
var result = await _eventRepository.GetManyByProviderActingUserAsync(providerUser.ProviderId,
|
var result = await _eventRepository.GetManyByProviderActingUserAsync(providerUser.ProviderId,
|
||||||
providerUser.UserId.Value, dateRange.Item1, dateRange.Item2,
|
providerUser.UserId.Value, dateRange.Item1, dateRange.Item2,
|
||||||
new PageOptions { ContinuationToken = continuationToken });
|
new PageOptions { ContinuationToken = continuationToken });
|
||||||
var responses = result.Data.Select(e => new EventResponseModel(e));
|
var responses = result.Data.Select(e => new EventResponseModel(e));
|
||||||
return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);
|
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 System.Text.Json;
|
||||||
using Azure.Messaging.EventGrid;
|
using Azure.Messaging.EventGrid;
|
||||||
using Azure.Messaging.EventGrid.SystemEvents;
|
using Azure.Messaging.EventGrid.SystemEvents;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -69,4 +70,35 @@ public static class ApiHelpers
|
|||||||
|
|
||||||
return new OkObjectResult(response);
|
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);
|
PageOptions pageOptions);
|
||||||
Task CreateAsync(IEvent e);
|
Task CreateAsync(IEvent e);
|
||||||
Task CreateManyAsync(IEnumerable<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);
|
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)
|
public async Task CreateAsync(IEvent e)
|
||||||
{
|
{
|
||||||
if (!(e is EventTableEntity entity))
|
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 ReadAccessTokens = new() { Name = nameof(ReadAccessTokens) };
|
||||||
public static readonly ServiceAccountOperationRequirement CreateAccessToken = new() { Name = nameof(CreateAccessToken) };
|
public static readonly ServiceAccountOperationRequirement CreateAccessToken = new() { Name = nameof(CreateAccessToken) };
|
||||||
public static readonly ServiceAccountOperationRequirement RevokeAccessTokens = new() { Name = nameof(RevokeAccessTokens) };
|
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,
|
private async Task<PagedResult<IEvent>> GetManyAsync(string sprocName,
|
||||||
IDictionary<string, object> sprocParams, DateTime startDate, DateTime endDate, PageOptions pageOptions)
|
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);
|
eventsTable.Columns.Add(ipAddressColumn);
|
||||||
var dateColumn = new DataColumn(nameof(e.Date), typeof(DateTime));
|
var dateColumn = new DataColumn(nameof(e.Date), typeof(DateTime));
|
||||||
eventsTable.Columns.Add(dateColumn);
|
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)
|
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[deviceTypeColumn] = ev.DeviceType.HasValue ? (object)ev.DeviceType.Value : DBNull.Value;
|
||||||
row[ipAddressColumn] = ev.IpAddress != null ? (object)ev.IpAddress : DBNull.Value;
|
row[ipAddressColumn] = ev.IpAddress != null ? (object)ev.IpAddress : DBNull.Value;
|
||||||
row[dateColumn] = ev.Date;
|
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);
|
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)
|
public async Task<PagedResult<IEvent>> GetManyByCipherAsync(Cipher cipher, DateTime startDate, DateTime endDate, PageOptions pageOptions)
|
||||||
{
|
{
|
||||||
DateTime? beforeDate = null;
|
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