diff --git a/src/Api/Dirt/Controllers/ReportsController.cs b/src/Api/Dirt/Controllers/ReportsController.cs
index 8bb8b5e487..e7c7e4a9bf 100644
--- a/src/Api/Dirt/Controllers/ReportsController.cs
+++ b/src/Api/Dirt/Controllers/ReportsController.cs
@@ -281,4 +281,127 @@ public class ReportsController : Controller
}
return await _getOrganizationReportQuery.GetLatestOrganizationReportAsync(orgId);
}
+
+ ///
+ /// Gets the Organization Report Summary for an organization.
+ /// This includes the latest report's encrypted data, encryption key, and date.
+ /// This is a mock implementation and should be replaced with actual data retrieval logic.
+ ///
+ ///
+ /// Min date (example: 2023-01-01)
+ /// Max date (example: 2023-12-31)
+ ///
+ ///
+ [HttpGet("organization-report-summary/{orgId}")]
+ public IEnumerable GetOrganizationReportSummary(
+ [FromRoute] Guid orgId,
+ [FromQuery] DateOnly from,
+ [FromQuery] DateOnly to)
+ {
+ if (!ModelState.IsValid)
+ {
+ throw new BadRequestException(ModelState);
+ }
+
+ GuardOrganizationAccess(orgId);
+
+ // FIXME: remove this mock class when actual data retrieval is implemented
+ return MockOrganizationReportSummary.GetMockData()
+ .Where(_ => _.OrganizationId == orgId
+ && _.Date >= from.ToDateTime(TimeOnly.MinValue)
+ && _.Date <= to.ToDateTime(TimeOnly.MaxValue));
+ }
+
+ ///
+ /// Creates a new Organization Report Summary for an organization.
+ /// This is a mock implementation and should be replaced with actual creation logic.
+ ///
+ ///
+ /// Returns 204 Created with the created OrganizationReportSummaryModel
+ ///
+ [HttpPost("organization-report-summary")]
+ public IActionResult CreateOrganizationReportSummary([FromBody] OrganizationReportSummaryModel model)
+ {
+ if (!ModelState.IsValid)
+ {
+ throw new BadRequestException(ModelState);
+ }
+
+ GuardOrganizationAccess(model.OrganizationId);
+
+ // TODO: Implement actual creation logic
+
+ // Returns 204 No Content as a placeholder
+ return NoContent();
+ }
+
+ [HttpPut("organization-report-summary")]
+ public IActionResult UpdateOrganizationReportSummary([FromBody] OrganizationReportSummaryModel model)
+ {
+ if (!ModelState.IsValid)
+ {
+ throw new BadRequestException(ModelState);
+ }
+
+ GuardOrganizationAccess(model.OrganizationId);
+
+ // TODO: Implement actual update logic
+
+ // Returns 204 No Content as a placeholder
+ return NoContent();
+ }
+
+ private void GuardOrganizationAccess(Guid organizationId)
+ {
+ if (!_currentContext.AccessReports(organizationId).Result)
+ {
+ throw new NotFoundException();
+ }
+ }
+
+ // FIXME: remove this mock class when actual data retrieval is implemented
+ private class MockOrganizationReportSummary
+ {
+ public static List GetMockData()
+ {
+ return new List
+ {
+ new OrganizationReportSummaryModel
+ {
+ OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"),
+ EncryptedData = "2.EtCcxDEBoF1MYChYHC4Q1w==|RyZ07R7qEFBbc/ICLFpEMockL9K+PD6rOod6DGHHrkaRLHUDqDwmxbu3jnD0cg8s7GIYmp0jApHXC+82QdApk87pA0Kr8fN2Rj0+8bDQCjhKfoRTipAB25S/n2E+ttjvlFfag92S66XqUH9S/eZw/Q==|0bPfykHk3SqS/biLNcNoYtH6YTstBEKu3AhvdZZLxhU=",
+ EncryptionKey = "2.Dd/TtdNwxWdYg9+fRkxh6w==|8KAiK9SoadgFRmyVOchd4tNh2vErD1Rv9x1gqtsE5tzxKE/V/5kkr1WuVG+QpEj//YaQt221UEMESRSXicZ7a9cB6xXLBkbbFwmecQRJVBs=|902em44n9cwciZzYrYuX6MRzRa+4hh1HHfNAxyJx/IM=",
+ Date = DateTime.UtcNow
+ },
+ new OrganizationReportSummaryModel
+ {
+ OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"),
+ EncryptedData = "2.HvY4fAvbzYV1hqa3255m5Q==|WcKga2Wka5i8fVso8MgjzfBAwxaqdhZDL3bnvhDsisZ0r9lNKQcG3YUQSFpJxr74cgg5QRQaFieCUe2YppciHDT6bsaE2VzFce3cNNB821uTFqnlJClkGJpG1nGvPupdErrg4Ik57WenEzYesmR4pw==|F0aJfF+1MlPm+eAlQnDgFnwfv198N9VtPqFJa4+UFqk=",
+ EncryptionKey = "2.ctMgLN4ycPusbQArG/uiag==|NtqiQsAoUxMSTBQsxAMyVLWdt5lVEUGZQNxZSBU4l76ywH2f6dx5FWFrcF3t3GBqy5yDoc5eBg0VlJDW9coqzp8j9n8h1iMrtmXPyBMAhbc=|pbH+w68BUdUKYCfNRpjd8NENw2lZ0vfxgMuTrsrRCTQ=",
+ Date = DateTime.UtcNow.AddMonths(-1)
+ },
+ new OrganizationReportSummaryModel
+ {
+ OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"),
+ EncryptedData = "2.NH4qLZYUkz/+qpB/mRsLTA==|LEFt05jJz0ngh+Hl5lqk6kebj7lZMefA3eFdL1kLJSGdD3uTOngRwH7GXLQNFeQOxutnLX9YUILbUEPwaM8gCwNQ1KWYdB1Z+Ky4nzKRb60N7L5aTA2za6zXTIdjv7Zwhg0jPZ6sPevTuvSyqjMCuA==|Uuu6gZaF0wvB2mHFwtvHegMxfe8DgsYWTRfGiVn4lkM=",
+ EncryptionKey = "2.3YwG78ykSxAn44NcymdG4w==|4jfn0nLoFielicAFbmq27DNUUjV4SwGePnjYRmOa7hk4pEPnQRS3MsTJFbutVyXOgKFY9Yn2yGFZownY9EmXOMM+gHPD0t6TfzUKqQcRyuI=|wasP9zZEL9mFH5HzJYrMxnKUr/XlFKXCxG9uW66uaPU=",
+ Date = DateTime.UtcNow.AddMonths(-1)
+ },
+ new OrganizationReportSummaryModel
+ {
+ OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"),
+ EncryptedData = "2.YmKWj/707wDPONh+JXPBOw==|Fx4jcUHmnUnSMCU8vdThMSYpDyKPnC09TxpSbNxia0M6MFbd5WHElcVribrYgTENyU0HlqPW43hThJ6xXCM0EjEWP7/jb/0l07vMNkA7sDYq+czf0XnYZgZSGKh06wFVz8xkhaPTdsiO4CXuMsoH+w==|DDVwVFHzdfbPQe3ycCx82eYVHDW97V/eWTPsNpHX/+U=",
+ EncryptionKey = "2.f/U45I7KF+JKfnvOArUyaw==|zNhhS2q2WwBl6SqLWMkxrXC8EX91Ra9LJExywkJhsRbxubRLt7fK+YWc8T1LUaDmMwJ3G8buSPGzyacKX0lnUR33dW6DIaLNgRZ/ekb/zkg=|qFoIZWwS0foiiIOyikFRwQKmmmI2HeyHcOVklJnIILI=",
+ Date = DateTime.UtcNow.AddMonths(-1)
+ },
+ new OrganizationReportSummaryModel
+ {
+ OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"),
+ EncryptedData = "2.WYauwooJUEY3kZsDPphmrA==|oguYW6h10A4GxK4KkRS0X32qSTekU2CkGqNDNGfisUgvJzsyoVTafO9sVcdPdg4BUM7YNkPMjYiKEc5jMHkIgLzbnM27jcGvMJrrccSrLHiWL6/mEiqQkV3TlfiZF9i3wqj1ITsYRzM454uNle6Wrg==|uR67aFYb1i5LSidWib0iTf8091l8GY5olHkVXse3CAw=",
+ EncryptionKey = "2.ZyV9+9A2cxNaf8dfzfbnlA==|hhorBpVkcrrhTtNmd6SNHYI8gPNokGLOC22Vx8Qa/AotDAcyuYWw56zsawMnzpAdJGEJFtszKM2+VUVOcroCTMWHpy8yNf/kZA6uPk3Lz3s=|ASzVeJf+K1ZB8NXuypamRBGRuRq0GUHZBEy5r/O7ORY=",
+ Date = DateTime.UtcNow.AddMonths(-1)
+ },
+ };
+ }
+ }
}
diff --git a/src/Api/Dirt/Models/Response/OrganizationReportSummaryModel.cs b/src/Api/Dirt/Models/Response/OrganizationReportSummaryModel.cs
new file mode 100644
index 0000000000..d912fb699e
--- /dev/null
+++ b/src/Api/Dirt/Models/Response/OrganizationReportSummaryModel.cs
@@ -0,0 +1,9 @@
+namespace Bit.Api.Dirt.Models.Response;
+
+public class OrganizationReportSummaryModel
+{
+ public Guid OrganizationId { get; set; }
+ public required string EncryptedData { get; set; }
+ public required string EncryptionKey { get; set; }
+ public DateTime Date { get; set; }
+}
diff --git a/test/Api.Test/Dirt/ReportsControllerTests.cs b/test/Api.Test/Dirt/ReportsControllerTests.cs
index af285d8b85..4636406df5 100644
--- a/test/Api.Test/Dirt/ReportsControllerTests.cs
+++ b/test/Api.Test/Dirt/ReportsControllerTests.cs
@@ -1,12 +1,14 @@
using AutoFixture;
using Bit.Api.Dirt.Controllers;
using Bit.Api.Dirt.Models;
+using Bit.Api.Dirt.Models.Response;
using Bit.Core.Context;
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Exceptions;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
+using Microsoft.AspNetCore.Mvc;
using NSubstitute;
using Xunit;
@@ -280,4 +282,185 @@ public class ReportsControllerTests
_ = sutProvider.GetDependency()
.Received(0);
}
+
+ [Theory, BitAutoData]
+ public void CreateOrganizationReportSummary_ReturnsNoContent_WhenAccessGranted(SutProvider sutProvider)
+ {
+ // Arrange
+ var orgId = Guid.NewGuid();
+ var model = new OrganizationReportSummaryModel
+ {
+ OrganizationId = orgId,
+ EncryptedData = "mock-data",
+ EncryptionKey = "mock-key",
+ Date = DateTime.UtcNow
+ };
+ sutProvider.GetDependency().AccessReports(orgId).Returns(true);
+
+ // Act
+ var result = sutProvider.Sut.CreateOrganizationReportSummary(model);
+
+ // Assert
+ Assert.IsType(result);
+ }
+
+ [Theory, BitAutoData]
+ public void CreateOrganizationReportSummary_ThrowsNotFoundException_WhenAccessDenied(SutProvider sutProvider)
+ {
+ // Arrange
+ var orgId = Guid.NewGuid();
+ var model = new OrganizationReportSummaryModel
+ {
+ OrganizationId = orgId,
+ EncryptedData = "mock-data",
+ EncryptionKey = "mock-key",
+ Date = DateTime.UtcNow
+ };
+ sutProvider.GetDependency().AccessReports(orgId).Returns(false);
+
+ // Act & Assert
+ Assert.Throws(
+ () => sutProvider.Sut.CreateOrganizationReportSummary(model));
+ }
+
+ [Theory, BitAutoData]
+ public void GetOrganizationReportSummary_ThrowsNotFoundException_WhenAccessDenied(
+ SutProvider sutProvider
+ )
+ {
+ // Arrange
+ var orgId = Guid.NewGuid();
+
+ sutProvider.GetDependency().AccessReports(orgId).Returns(false);
+
+ // Act & Assert
+ Assert.Throws(
+ () => sutProvider.Sut.GetOrganizationReportSummary(orgId, DateOnly.FromDateTime(DateTime.UtcNow), DateOnly.FromDateTime(DateTime.UtcNow)));
+ }
+
+ [Theory, BitAutoData]
+ public void GetOrganizationReportSummary_returnsExpectedResult(
+ SutProvider sutProvider
+ )
+ {
+ // Arrange
+ var orgId = Guid.NewGuid();
+ var dates = new[]
+ {
+ DateOnly.FromDateTime(DateTime.UtcNow),
+ DateOnly.FromDateTime(DateTime.UtcNow.AddMonths(-1))
+ };
+
+ sutProvider.GetDependency().AccessReports(orgId).Returns(true);
+
+ // Act
+ var result = sutProvider.Sut.GetOrganizationReportSummary(orgId, dates[0], dates[1]);
+
+ // Assert
+ Assert.NotNull(result);
+ }
+
+ [Theory, BitAutoData]
+ public void CreateOrganizationReportSummary_ReturnsNoContent_WhenModelIsValidAndAccessGranted(
+ SutProvider sutProvider
+ )
+ {
+ // Arrange
+ var orgId = Guid.NewGuid();
+ var model = new OrganizationReportSummaryModel
+ {
+ OrganizationId = orgId,
+ EncryptedData = "mock-data",
+ EncryptionKey = "mock-key"
+ };
+ sutProvider.Sut.ModelState.Clear();
+ sutProvider.GetDependency().AccessReports(orgId).Returns(true);
+
+ // Act
+ var result = sutProvider.Sut.CreateOrganizationReportSummary(model);
+
+ // Assert
+ Assert.IsType(result);
+ }
+
+ [Theory, BitAutoData]
+ public void CreateOrganizationReportSummary_ThrowsBadRequestException_WhenModelStateIsInvalid(
+ SutProvider sutProvider
+ )
+ {
+ // Arrange
+ var orgId = Guid.NewGuid();
+ var model = new OrganizationReportSummaryModel
+ {
+ OrganizationId = orgId,
+ EncryptedData = "mock-data",
+ EncryptionKey = "mock-key"
+ };
+ sutProvider.Sut.ModelState.AddModelError("key", "error");
+
+ // Act & Assert
+ Assert.Throws(() => sutProvider.Sut.CreateOrganizationReportSummary(model));
+ }
+
+ [Theory, BitAutoData]
+ public void UpdateOrganizationReportSummary_ReturnsNoContent_WhenModelIsValidAndAccessGranted(
+ SutProvider sutProvider
+ )
+ {
+ // Arrange
+ var orgId = Guid.NewGuid();
+ var model = new OrganizationReportSummaryModel
+ {
+ OrganizationId = orgId,
+ EncryptedData = "mock-data",
+ EncryptionKey = "mock-key"
+ };
+ sutProvider.Sut.ModelState.Clear();
+ sutProvider.GetDependency().AccessReports(orgId).Returns(true);
+
+ // Act
+ var result = sutProvider.Sut.UpdateOrganizationReportSummary(model);
+
+ // Assert
+ Assert.IsType(result);
+ }
+
+ [Theory, BitAutoData]
+ public void UpdateOrganizationReportSummary_ThrowsBadRequestException_WhenModelStateIsInvalid(
+ SutProvider sutProvider
+ )
+ {
+ // Arrange
+ var orgId = Guid.NewGuid();
+ var model = new OrganizationReportSummaryModel
+ {
+ OrganizationId = orgId,
+ EncryptedData = "mock-data",
+ EncryptionKey = "mock-key"
+ };
+ sutProvider.Sut.ModelState.AddModelError("key", "error");
+
+ // Act & Assert
+ Assert.Throws(() => sutProvider.Sut.UpdateOrganizationReportSummary(model));
+ }
+
+ [Theory, BitAutoData]
+ public void UpdateOrganizationReportSummary_ThrowsNotFoundException_WhenAccessDenied(
+ SutProvider sutProvider
+ )
+ {
+ // Arrange
+ var orgId = Guid.NewGuid();
+ var model = new OrganizationReportSummaryModel
+ {
+ OrganizationId = orgId,
+ EncryptedData = "mock-data",
+ EncryptionKey = "mock-key"
+ };
+ sutProvider.Sut.ModelState.Clear();
+ sutProvider.GetDependency().AccessReports(orgId).Returns(false);
+
+ // Act & Assert
+ Assert.Throws(() => sutProvider.Sut.UpdateOrganizationReportSummary(model));
+ }
}