1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-25 13:18:48 -05:00

[PM-20577] OrganizationReport endpoints (#5986)

* PM-20574 fixing namespaces on reporting work that got moved over from tools

* PM-20574 adding tables, stored procedures, and migration files

* PM-20574 adding dapper and ef repos and migrations

* PM-20574 changing table and repo names as requested

* PM-20574 updating sql scripts to new names

* PM-20574 updating sql scripts

* PM-20574 updating migration script for org delete by id

* PM-20574 adding mysql migration

* PM-20574 updating sql migration to fix database test

* PM-20574 fixing migration script

* PM-20574 fixing migration script

* PM-20574 fixing table scripts

* PM-20574 fixing table scripts

* PM-20574 fixing migration script formatting

* PM-20574 fixing syntax in migration script

* PM-20574 fixing file names and extensions

* PM-20574 fixing sql file

* PM-20574 fixing sql

* PM-20574 fixing directory for entities and removing scripts from other databases

* PM-20574 generating new migration scripts

* PM-20574 fixed reference to a stored proc

* PM-20574 adding index in scripts and missing table

* PM-20574 fixing merge conflicts

* PM-20574 set OUTPUT param for Id property in create and update proc

* PM-20574 add CreateDate to the update proc

* PM-20574 amend update proc for OrganizationApplication by adding createDate

* PM-20576 Created OrganizationReportRepo and unit tests

* PM-20576 Commands and Query for OrganizationReport

* PM-20576 added additional unit tests to fix CodeCoverage report

* PM-20574 formatted sql and updated as per PR comments

* PM-20574 updated script to fix build error

* PM-20574 fixed inconsistency in db script

* PM-20577 organization-reports endpoints

* PM-20574 removed revisionDate, update procedures and used views

* PM-20574 removed RevisionDate from designer files

* PM-20574 removed revisionDate column that was missed previously

* PM-20574 added revision date back into the mix

* PM-20574 updated database script to fix build error

* PM-20574 fixed a procedure issue

* PM-20574 fix dB build error

* PM-020574 fixed additional PR comments - files cleaned up

* PM-20574 updated procedure was inconsistent

* PM-20576 added logs and updated errors as per PR comments

* PM-20576 fixed a build error

* PM-20576 removed RevisionDate from Repo and tests

* PM-20576 added dependency

* PM-20576 removed unwanted line from csproj file

---------

Co-authored-by: Graham Walker <gwalker@bitwarden.com>
Co-authored-by: Tom <144813356+ttalty@users.noreply.github.com>
This commit is contained in:
Vijay Oommen 2025-06-24 14:53:04 -05:00 committed by GitHub
parent 51e93c7323
commit 74964bf170
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 225 additions and 7 deletions

View File

@ -23,6 +23,9 @@ public class ReportsController : Controller
private readonly IAddPasswordHealthReportApplicationCommand _addPwdHealthReportAppCommand;
private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery;
private readonly IDropPasswordHealthReportApplicationCommand _dropPwdHealthReportAppCommand;
private readonly IAddOrganizationReportCommand _addOrganizationReportCommand;
private readonly IDropOrganizationReportCommand _dropOrganizationReportCommand;
private readonly IGetOrganizationReportQuery _getOrganizationReportQuery;
public ReportsController(
ICurrentContext currentContext,
@ -30,7 +33,10 @@ public class ReportsController : Controller
IRiskInsightsReportQuery riskInsightsReportQuery,
IAddPasswordHealthReportApplicationCommand addPasswordHealthReportApplicationCommand,
IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery,
IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand
IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand,
IGetOrganizationReportQuery getOrganizationReportQuery,
IAddOrganizationReportCommand addOrganizationReportCommand,
IDropOrganizationReportCommand dropOrganizationReportCommand
)
{
_currentContext = currentContext;
@ -39,6 +45,9 @@ public class ReportsController : Controller
_addPwdHealthReportAppCommand = addPasswordHealthReportApplicationCommand;
_getPwdHealthReportAppQuery = getPasswordHealthReportApplicationQuery;
_dropPwdHealthReportAppCommand = dropPwdHealthReportAppCommand;
_getOrganizationReportQuery = getOrganizationReportQuery;
_addOrganizationReportCommand = addOrganizationReportCommand;
_dropOrganizationReportCommand = dropOrganizationReportCommand;
}
/// <summary>
@ -204,4 +213,72 @@ public class ReportsController : Controller
await _dropPwdHealthReportAppCommand.DropPasswordHealthReportApplicationAsync(request);
}
/// <summary>
/// Adds a new organization report
/// </summary>
/// <param name="request">A single instance of AddOrganizationReportRequest</param>
/// <returns>A single instance of OrganizationReport</returns>
/// <exception cref="NotFoundException">If user does not have access to the organization</exception>
/// <exception cref="BadRequestException">If the organization Id is not valid</exception>
[HttpPost("organization-reports")]
public async Task<OrganizationReport> AddOrganizationReport([FromBody] AddOrganizationReportRequest request)
{
if (!await _currentContext.AccessReports(request.OrganizationId))
{
throw new NotFoundException();
}
return await _addOrganizationReportCommand.AddOrganizationReportAsync(request);
}
/// <summary>
/// Drops organization reports for an organization
/// </summary>
/// <param name="request">A single instance of DropOrganizationReportRequest</param>
/// <returns></returns>
/// <exception cref="NotFoundException">If user does not have access to the organization</exception>
/// <exception cref="BadRequestException">If the organization does not have any records</exception>
[HttpDelete("organization-reports")]
public async Task DropOrganizationReport([FromBody] DropOrganizationReportRequest request)
{
if (!await _currentContext.AccessReports(request.OrganizationId))
{
throw new NotFoundException();
}
await _dropOrganizationReportCommand.DropOrganizationReportAsync(request);
}
/// <summary>
/// Gets organization reports for an organization
/// </summary>
/// <param name="orgId">A valid Organization Id</param>
/// <returns>An Enumerable of OrganizationReport</returns>
/// <exception cref="NotFoundException">If user does not have access to the organization</exception>
/// <exception cref="BadRequestException">If the organization Id is not valid</exception>
[HttpGet("organization-reports/{orgId}")]
public async Task<IEnumerable<OrganizationReport>> GetOrganizationReports(Guid orgId)
{
if (!await _currentContext.AccessReports(orgId))
{
throw new NotFoundException();
}
return await _getOrganizationReportQuery.GetOrganizationReportAsync(orgId);
}
/// <summary>
/// Gets the latest organization report for an organization
/// </summary>
/// <param name="orgId">A valid Organization Id</param>
/// <returns>A single instance of OrganizationReport</returns>
/// <exception cref="NotFoundException">If user does not have access to the organization</exception>
/// <exception cref="BadRequestException">If the organization Id is not valid</exception>
[HttpGet("organization-reports/latest/{orgId}")]
public async Task<OrganizationReport> GetLatestOrganizationReport(Guid orgId)
{
if (!await _currentContext.AccessReports(orgId))
{
throw new NotFoundException();
}
return await _getOrganizationReportQuery.GetLatestOrganizationReportAsync(orgId);
}
}

View File

@ -31,12 +31,15 @@ public class DropOrganizationReportCommand : IDropOrganizationReportCommand
throw new BadRequestException("No data found.");
}
data.Where(_ => request.OrganizationReportIds.Contains(_.Id)).ToList().ForEach(async _ =>
{
_logger.LogInformation("Dropping organization report {organizationReportId} for organization {organizationId}",
_.Id, request.OrganizationId);
data
.Where(_ => request.OrganizationReportIds.Contains(_.Id))
.ToList()
.ForEach(async reportId =>
{
_logger.LogInformation("Dropping organization report {organizationReportId} for organization {organizationId}",
reportId, request.OrganizationId);
await _organizationReportRepo.DeleteAsync(_);
});
await _organizationReportRepo.DeleteAsync(reportId);
});
}
}

View File

@ -142,4 +142,142 @@ public class ReportsControllerTests
_.OrganizationId == request.OrganizationId &&
_.PasswordHealthReportApplicationIds == request.PasswordHealthReportApplicationIds));
}
[Theory, BitAutoData]
public async Task AddOrganizationReportAsync_withAccess_success(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);
// Act
var request = new AddOrganizationReportRequest
{
OrganizationId = Guid.NewGuid(),
ReportData = "Report Data",
Date = DateTime.UtcNow
};
await sutProvider.Sut.AddOrganizationReport(request);
// Assert
_ = sutProvider.GetDependency<IAddOrganizationReportCommand>()
.Received(1)
.AddOrganizationReportAsync(Arg.Is<AddOrganizationReportRequest>(_ =>
_.OrganizationId == request.OrganizationId &&
_.ReportData == request.ReportData &&
_.Date == request.Date));
}
[Theory, BitAutoData]
public async Task AddOrganizationReportAsync_withoutAccess(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false);
// Act
var request = new AddOrganizationReportRequest
{
OrganizationId = Guid.NewGuid(),
ReportData = "Report Data",
Date = DateTime.UtcNow
};
await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.AddOrganizationReport(request));
// Assert
_ = sutProvider.GetDependency<IAddOrganizationReportCommand>()
.Received(0);
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withAccess_success(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);
// Act
var request = new DropOrganizationReportRequest
{
OrganizationId = Guid.NewGuid(),
OrganizationReportIds = new List<Guid> { Guid.NewGuid(), Guid.NewGuid() }
};
await sutProvider.Sut.DropOrganizationReport(request);
// Assert
_ = sutProvider.GetDependency<IDropOrganizationReportCommand>()
.Received(1)
.DropOrganizationReportAsync(Arg.Is<DropOrganizationReportRequest>(_ =>
_.OrganizationId == request.OrganizationId &&
_.OrganizationReportIds.SequenceEqual(request.OrganizationReportIds)));
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withoutAccess(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false);
// Act
var request = new DropOrganizationReportRequest
{
OrganizationId = Guid.NewGuid(),
OrganizationReportIds = new List<Guid> { Guid.NewGuid(), Guid.NewGuid() }
};
await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.DropOrganizationReport(request));
// Assert
_ = sutProvider.GetDependency<IDropOrganizationReportCommand>()
.Received(0);
}
[Theory, BitAutoData]
public async Task GetOrganizationReportAsync_withAccess_success(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);
// Act
var orgId = Guid.NewGuid();
var result = await sutProvider.Sut.GetOrganizationReports(orgId);
// Assert
_ = sutProvider.GetDependency<IGetOrganizationReportQuery>()
.Received(1)
.GetOrganizationReportAsync(Arg.Is<Guid>(_ => _ == orgId));
}
[Theory, BitAutoData]
public async Task GetOrganizationReportAsync_withoutAccess(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false);
// Act
var orgId = Guid.NewGuid();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetOrganizationReports(orgId));
// Assert
_ = sutProvider.GetDependency<IGetOrganizationReportQuery>()
.Received(0);
}
[Theory, BitAutoData]
public async Task GetLastestOrganizationReportAsync_withAccess_success(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);
// Act
var orgId = Guid.NewGuid();
var result = await sutProvider.Sut.GetLatestOrganizationReport(orgId);
// Assert
_ = sutProvider.GetDependency<IGetOrganizationReportQuery>()
.Received(1)
.GetLatestOrganizationReportAsync(Arg.Is<Guid>(_ => _ == orgId));
}
[Theory, BitAutoData]
public async Task GetLastestOrganizationReportAsync_withoutAccess(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false);
// Act
var orgId = Guid.NewGuid();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetLatestOrganizationReport(orgId));
// Assert
_ = sutProvider.GetDependency<IGetOrganizationReportQuery>()
.Received(0);
}
}