Skip to content
4 changes: 3 additions & 1 deletion src/Api/Dirt/Public/Controllers/EventsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ public EventsController(
/// </summary>
/// <remarks>
/// Returns a filtered list of your organization's event logs, paged by a continuation token.
/// If no filters are provided, it will return the last 30 days of event for the organization.
/// If no date filters are provided, it will return the last 30 days of events for the organization.
/// Providing only <c>start</c> returns events from then through the current time;
/// providing only <c>end</c> returns the 30 days before it. A range greater than 367 days is rejected.
/// </remarks>
[HttpGet]
[ProducesResponseType(typeof(PagedListResponseModel<EventResponseModel>), (int)HttpStatusCode.OK)]
Expand Down
28 changes: 7 additions & 21 deletions src/Api/Dirt/Public/Models/EventFilterRequestModel.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
ο»Ώ// FIXME: Update this file to be null safe and then delete the line below
#nullable disable

using Bit.Core.Exceptions;
using Bit.Api.Utilities;

namespace Bit.Api.Dirt.Public.Models;

public class EventFilterRequestModel
{
/// <summary>
/// The start date. Must be less than the end date.
/// The start date. If omitted, defaults to 30 days before the end date (or 30 days ago when no end date is given).
/// </summary>
public DateTime? Start { get; set; }
/// <summary>
/// The end date. Must be greater than the start date.
/// The end date. If omitted, defaults to the current time.
/// </summary>
public DateTime? End { get; set; }
/// <summary>
Expand All @@ -38,23 +38,9 @@ public class EventFilterRequestModel

public Tuple<DateTime, DateTime> ToDateRange()
{
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("Date range must be < 367 days.");
}

return new Tuple<DateTime, DateTime>(Start.Value, End.Value);
var dateRange = ApiHelpers.GetDateRange(Start, End);
Start = dateRange.Item1;
End = dateRange.Item2;
return dateRange;
}
}
26 changes: 16 additions & 10 deletions src/Api/Utilities/ApiHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,28 +80,34 @@ public async static Task<ObjectResult> HandleAzureEvents(HttpRequest request,
/// <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.
/// A supplied bound is always honored; the missing bound is inferred from it.
/// With neither supplied, returns the last 30 days.
/// With only <paramref name="start"/>, the range runs to the current time.
/// With only <paramref name="end"/>, the range covers the 30 days before it.
/// An inverted range is swapped. A range greater than 367 days throws BadRequestException.
/// </remarks>
public static Tuple<DateTime, DateTime> GetDateRange(DateTime? start, DateTime? end)
{
if (!end.HasValue || !start.HasValue)
if (!start.HasValue)
{
end = DateTime.UtcNow.Date.AddDays(1).AddMilliseconds(-1);
start = DateTime.UtcNow.Date.AddDays(-30);
start = end.HasValue ? ThirtyDaysBefore(end.Value) : DateTime.UtcNow.Date.AddDays(-30);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: you could move the assignment of end to above this check, and then this line could be simplified to:

Suggested change
start = end.HasValue ? ThirtyDaysBefore(end.Value) : DateTime.UtcNow.Date.AddDays(-30);
start = ThirtyDaysBefore(end.Value);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wouldn't keep the same value, since .Date evaluates to the very beginning datetime for the date it is read from, but that should be no issue. It would be a true 30 days clock time back instead.

}
else if (start.Value > end.Value)

end ??= DateTime.UtcNow;

if (start.Value > end.Value)
{
var newEnd = start;
start = end;
end = newEnd;
(start, end) = (end, start);
}
Comment on lines +98 to 101

@lastbestdev lastbestdev Sep 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This functionality is interesting to me. I know it was existing so perhaps we leave it alone, but I would personally prefer our API to return a 400 Bad Request to the caller when incorrect date parameters are provided.

This makes it clear that the consumer of the API is responsible for providing a sensible date range, and doesn't let them build bad integrations with our API that mix up the start/end params

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree in principle but I think this might be out of scope/need a separate ticket because main is accounting for this backward date correction and so it could break existing code that is supplying the backwards dates. So everyone's code that is supplying bad dates would then break with no warning. What do you think? Do you think it's enough of an edge case to just implement it in this PR and maybe a couple people learn the hard way? @lastbestdev

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this bit is out of scope, and it would be something to discuss with the team if we should remove it. To your point, doing this should include the due diligence of checking our clients references to the endpoints providing date range parameters and ensuring they don't pass the start/end backwards


if ((end.Value - start.Value) > TimeSpan.FromDays(367))
{
throw new BadRequestException("Range too large.");
throw new BadRequestException("Date range must be < 367 days.");
}

return new Tuple<DateTime, DateTime>(start.Value, end.Value);
}

private static DateTime ThirtyDaysBefore(DateTime value) =>
value - DateTime.MinValue < TimeSpan.FromDays(30) ? DateTime.MinValue : value.AddDays(-30);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
ο»Ώusing Bit.Api.Dirt.Public.Models;
using Xunit;

namespace Bit.Api.Test.Dirt.Public.Models;

public class EventFilterRequestModelTests
{
[Fact]
public void ToDateRange_OnlyStartSupplied_DoesNotFallBackToThirtyDayDefault()
{
var suppliedStart = DateTime.UtcNow.AddDays(-3);
var request = new EventFilterRequestModel { Start = suppliedStart };

var dateRange = request.ToDateRange();

Assert.Equal(suppliedStart, dateRange.Item1);
}

[Fact]
public void ToDateRange_WritesResolvedBoundsBackOntoTheModelForDiagnosticLogging()
{
var request = new EventFilterRequestModel();

var dateRange = request.ToDateRange();

Assert.Equal(dateRange.Item1, request.Start);
Assert.Equal(dateRange.Item2, request.End);
}
}
117 changes: 117 additions & 0 deletions test/Api.Test/Utilities/ApiHelpersTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
ο»Ώusing System.Text;
using Bit.Api.Utilities;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Exceptions;
using Microsoft.AspNetCore.Http;
using NSubstitute;
using Xunit;
Expand All @@ -22,5 +23,121 @@ public async Task ReadJsonFileFromBody_Success()
Assert.Equal(8, license.Version);
}

[Fact]
public void GetDateRange_NeitherBoundSupplied_ReturnsLastThirtyDays()
{
var before = DateTime.UtcNow;

var (start, end) = ApiHelpers.GetDateRange(null, null);

Assert.Equal(DateTime.UtcNow.Date.AddDays(-30), start);
Assert.InRange(end, before, DateTime.UtcNow);
}

[Fact]
public void GetDateRange_OnlyStartSupplied_KeepsStartAndRunsToNow()
{
var suppliedStart = DateTime.UtcNow.AddDays(-3);
var before = DateTime.UtcNow;

var (start, end) = ApiHelpers.GetDateRange(suppliedStart, null);

Assert.Equal(suppliedStart, start);
Assert.InRange(end, before, DateTime.UtcNow);
}

[Fact]
public void GetDateRange_OnlyEndSupplied_KeepsEndAndStartsThirtyDaysBefore()
{
var suppliedEnd = new DateTime(2026, 6, 8, 14, 9, 35, DateTimeKind.Utc);

var (start, end) = ApiHelpers.GetDateRange(null, suppliedEnd);

Assert.Equal(suppliedEnd.AddDays(-30), start);
Assert.Equal(suppliedEnd, end);
}

[Fact]
public void GetDateRange_OnlyEndSuppliedNearMinValue_ClampsStartInsteadOfThrowing()
{
var suppliedEnd = new DateTime(1, 1, 10, 0, 0, 0, DateTimeKind.Utc);

var (start, end) = ApiHelpers.GetDateRange(null, suppliedEnd);

Assert.Equal(DateTime.MinValue, start);
Assert.Equal(suppliedEnd, end);
}

[Fact]
public void GetDateRange_OnlyEndSuppliedExactlyThirtyDaysAfterMinValue_ClampsToMinValue()
{
var suppliedEnd = DateTime.MinValue.AddDays(30);

var (start, _) = ApiHelpers.GetDateRange(null, suppliedEnd);

Assert.Equal(DateTime.MinValue, start);
}

[Fact]
public void GetDateRange_OnlyStartSuppliedAtMaxValue_ThrowsBadRequestRatherThanOverflowing()
{
Assert.Throws<BadRequestException>(() => ApiHelpers.GetDateRange(DateTime.MaxValue, null));
}

[Fact]
public void GetDateRange_BothBoundsSupplied_ReturnsThemUnchanged()
{
var suppliedStart = new DateTime(2026, 6, 1, 0, 0, 0, DateTimeKind.Utc);
var suppliedEnd = new DateTime(2026, 6, 8, 0, 0, 0, DateTimeKind.Utc);

var (start, end) = ApiHelpers.GetDateRange(suppliedStart, suppliedEnd);

Assert.Equal(suppliedStart, start);
Assert.Equal(suppliedEnd, end);
}

[Fact]
public void GetDateRange_InvertedBounds_SwapsThem()
{
var earlier = new DateTime(2026, 6, 1, 0, 0, 0, DateTimeKind.Utc);
var later = new DateTime(2026, 6, 8, 0, 0, 0, DateTimeKind.Utc);

var (start, end) = ApiHelpers.GetDateRange(later, earlier);

Assert.Equal(earlier, start);
Assert.Equal(later, end);
}

[Fact]
public void GetDateRange_RangeExceedsCap_ThrowsBadRequest()
{
var suppliedStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var suppliedEnd = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);

var exception = Assert.Throws<BadRequestException>(
() => ApiHelpers.GetDateRange(suppliedStart, suppliedEnd));

Assert.Equal("Date range must be < 367 days.", exception.Message);
}

[Fact]
public void GetDateRange_OnlyStartSuppliedBeyondCap_ThrowsBadRequest()
{
var suppliedStart = DateTime.UtcNow.AddDays(-400);

Assert.Throws<BadRequestException>(() => ApiHelpers.GetDateRange(suppliedStart, null));
}

[Fact]
public void GetDateRange_OnlyStartSuppliedInTheFuture_SwapsRatherThanInverting()
{
var suppliedStart = DateTime.UtcNow.AddDays(3);

var (start, end) = ApiHelpers.GetDateRange(suppliedStart, null);

Assert.True(start <= end);
Assert.Equal(suppliedStart, end);
}

const string testFile = "{\"licenseKey\": \"licenseKey\", \"installationId\": \"6285f891-b2ec-4047-84c5-2eb7f7747e74\", \"id\": \"1065216d-5854-4326-838d-635487f30b43\",\"name\": \"Test Org\",\"billingEmail\": \"test@email.com\",\"businessName\": null,\"enabled\": true, \"plan\": \"Enterprise (Annually)\",\"planType\": 11,\"seats\": 6,\"maxCollections\": null,\"usePolicies\": true,\"useSso\": true,\"useKeyConnector\": false,\"useGroups\": true,\"useEvents\": true,\"useDirectory\": true,\"useTotp\": true,\"use2fa\": true,\"useApi\": true,\"useResetPassword\": true,\"maxStorageGb\": 1,\"selfHost\": true,\"usersGetPremium\": true,\"version\": 8,\"issued\": \"2022-01-25T21:58:38.9454581Z\",\"refresh\": \"2022-01-28T14:26:31Z\",\"expires\": \"2022-01-28T14:26:31Z\",\"trial\": true,\"hash\": \"testvalue\",\"signature\": \"signature\"}";
}
Loading