Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 220 additions & 25 deletions src/Api/Dirt/Controllers/OrganizationReportsController.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
using Bit.Api.Dirt.Models.Response;
using System.Text.Json;
using Bit.Api.Dirt.Models.Response;
using Bit.Api.Utilities;
using Bit.Core;
using Bit.Core.Context;
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Dirt.Reports.Services;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -23,6 +30,15 @@ public class OrganizationReportsController : Controller
private readonly IUpdateOrganizationReportDataCommand _updateOrganizationReportDataCommand;
private readonly IGetOrganizationReportApplicationDataQuery _getOrganizationReportApplicationDataQuery;
private readonly IUpdateOrganizationReportApplicationDataCommand _updateOrganizationReportApplicationDataCommand;
private readonly IFeatureService _featureService;
private readonly IApplicationCacheService _applicationCacheService;
private readonly IOrganizationReportStorageService _storageService;
private readonly ICreateOrganizationReportV2Command _createV2Command;
private readonly IUpdateOrganizationReportDataV2Command _updateDataV2Command;
private readonly IGetOrganizationReportDataV2Query _getDataV2Query;
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: _getDataV2Query is injected but never called anywhere in this controller or the codebase. The GetOrganizationReportDataAsync GET endpoint (line 263) has no V2 feature-flag branch, so there's no way for clients to retrieve a download URL for V2 report data files.

This appears to be a missing V2 read endpoint — should the GET at line 263 have a WholeReportDataFileStorage branch that calls _getDataV2Query.GetOrganizationReportDataAsync() (similar to how create/update endpoints have V2 branches)?

private readonly IOrganizationReportRepository _organizationReportRepo;
private readonly IValidateOrganizationReportFileCommand _validateCommand;
private readonly ILogger<OrganizationReportsController> _logger;

public OrganizationReportsController(
ICurrentContext currentContext,
Expand All @@ -35,8 +51,16 @@ public OrganizationReportsController(
IGetOrganizationReportDataQuery getOrganizationReportDataQuery,
IUpdateOrganizationReportDataCommand updateOrganizationReportDataCommand,
IGetOrganizationReportApplicationDataQuery getOrganizationReportApplicationDataQuery,
IUpdateOrganizationReportApplicationDataCommand updateOrganizationReportApplicationDataCommand
)
IUpdateOrganizationReportApplicationDataCommand updateOrganizationReportApplicationDataCommand,
IFeatureService featureService,
IApplicationCacheService applicationCacheService,
IOrganizationReportStorageService storageService,
ICreateOrganizationReportV2Command createV2Command,
IUpdateOrganizationReportDataV2Command updateDataV2Command,
IGetOrganizationReportDataV2Query getDataV2Query,
IOrganizationReportRepository organizationReportRepo,
IValidateOrganizationReportFileCommand validateCommand,
ILogger<OrganizationReportsController> logger)
{
_currentContext = currentContext;
_getOrganizationReportQuery = getOrganizationReportQuery;
Expand All @@ -49,10 +73,17 @@ IUpdateOrganizationReportApplicationDataCommand updateOrganizationReportApplicat
_updateOrganizationReportDataCommand = updateOrganizationReportDataCommand;
_getOrganizationReportApplicationDataQuery = getOrganizationReportApplicationDataQuery;
_updateOrganizationReportApplicationDataCommand = updateOrganizationReportApplicationDataCommand;
_featureService = featureService;
_applicationCacheService = applicationCacheService;
_storageService = storageService;
_createV2Command = createV2Command;
_updateDataV2Command = updateDataV2Command;
_getDataV2Query = getDataV2Query;
_organizationReportRepo = organizationReportRepo;
_validateCommand = validateCommand;
_logger = logger;
}

#region Whole OrganizationReport Endpoints

[HttpGet("{organizationId}/latest")]
public async Task<IActionResult> GetLatestOrganizationReportAsync(Guid organizationId)
{
Expand All @@ -70,29 +101,70 @@ public async Task<IActionResult> GetLatestOrganizationReportAsync(Guid organizat
[HttpGet("{organizationId}/{reportId}")]
public async Task<IActionResult> GetOrganizationReportAsync(Guid organizationId, Guid reportId)
{
if (_featureService.IsEnabled(FeatureFlagKeys.WholeReportDataFileStorage))
{
await AuthorizeV2Async(organizationId);

var report = await _getOrganizationReportQuery.GetOrganizationReportAsync(reportId);

if (report.OrganizationId != organizationId)
{
throw new BadRequestException("Invalid report ID");
}

return Ok(new OrganizationReportResponseModel(report));
}

if (!await _currentContext.AccessReports(organizationId))
{
throw new NotFoundException();
}

var report = await _getOrganizationReportQuery.GetOrganizationReportAsync(reportId);
var v1Report = await _getOrganizationReportQuery.GetOrganizationReportAsync(reportId);

if (report == null)
if (v1Report == null)
{
throw new NotFoundException("Report not found for the specified organization.");
}

if (report.OrganizationId != organizationId)
if (v1Report.OrganizationId != organizationId)
{
throw new BadRequestException("Invalid report ID");
}

return Ok(report);
return Ok(v1Report);
}

[HttpPost("{organizationId}")]
public async Task<IActionResult> CreateOrganizationReportAsync(Guid organizationId, [FromBody] AddOrganizationReportRequest request)
public async Task<IActionResult> CreateOrganizationReportAsync(
Guid organizationId,
[FromBody] AddOrganizationReportRequest request)
{
if (_featureService.IsEnabled(FeatureFlagKeys.WholeReportDataFileStorage))
{
if (organizationId == Guid.Empty)
{
throw new BadRequestException("Organization ID is required.");
}

if (request.OrganizationId != organizationId)
{
throw new BadRequestException("Organization ID in the request body must match the route parameter");
}

await AuthorizeV2Async(organizationId);

var report = await _createV2Command.CreateAsync(request);
var fileData = report.GetReportFileData()!;

return Ok(new OrganizationReportV2ResponseModel
{
ReportDataUploadUrl = await _storageService.GetReportDataUploadUrlAsync(report, fileData),
ReportResponse = new OrganizationReportResponseModel(report),
FileUploadType = _storageService.FileUploadType
});
}

if (!await _currentContext.AccessReports(organizationId))
{
throw new NotFoundException();
Expand All @@ -103,8 +175,8 @@ public async Task<IActionResult> CreateOrganizationReportAsync(Guid organization
throw new BadRequestException("Organization ID in the request body must match the route parameter");
}

var report = await _addOrganizationReportCommand.AddOrganizationReportAsync(request);
var response = report == null ? null : new OrganizationReportResponseModel(report);
var v1Report = await _addOrganizationReportCommand.AddOrganizationReportAsync(request);
var response = v1Report == null ? null : new OrganizationReportResponseModel(v1Report);
return Ok(response);
}

Expand All @@ -126,10 +198,6 @@ public async Task<IActionResult> UpdateOrganizationReportAsync(Guid organization
return Ok(response);
}

#endregion

# region SummaryData Field Endpoints

[HttpGet("{organizationId}/data/summary")]
public async Task<IActionResult> GetOrganizationReportSummaryDataByDateRangeAsync(
Guid organizationId, [FromQuery] DateTime startDate, [FromQuery] DateTime endDate)
Expand Down Expand Up @@ -191,9 +259,6 @@ public async Task<IActionResult> UpdateOrganizationReportSummaryAsync(Guid organ

return Ok(response);
}
#endregion

#region ReportData Field Endpoints

[HttpGet("{organizationId}/data/report/{reportId}")]
public async Task<IActionResult> GetOrganizationReportDataAsync(Guid organizationId, Guid reportId)
Expand All @@ -214,8 +279,37 @@ public async Task<IActionResult> GetOrganizationReportDataAsync(Guid organizatio
}

[HttpPatch("{organizationId}/data/report/{reportId}")]
public async Task<IActionResult> UpdateOrganizationReportDataAsync(Guid organizationId, Guid reportId, [FromBody] UpdateOrganizationReportDataRequest request)
public async Task<IActionResult> UpdateOrganizationReportDataAsync(
Guid organizationId,
Guid reportId,
[FromBody] UpdateOrganizationReportDataRequest request,
[FromQuery] string? reportFileId)
{
if (_featureService.IsEnabled(FeatureFlagKeys.WholeReportDataFileStorage))
{
if (request.OrganizationId != organizationId || request.ReportId != reportId)
{
throw new BadRequestException("Organization ID and Report ID must match route parameters");
}

if (string.IsNullOrEmpty(reportFileId))
{
throw new BadRequestException("ReportFileId query parameter is required");
}

await AuthorizeV2Async(organizationId);

var uploadUrl = await _updateDataV2Command.GetUploadUrlAsync(request, reportFileId);
var report = await _getOrganizationReportQuery.GetOrganizationReportAsync(reportId);

return Ok(new OrganizationReportV2ResponseModel
{
ReportDataUploadUrl = uploadUrl,
ReportResponse = new OrganizationReportResponseModel(report),
FileUploadType = _storageService.FileUploadType
});
}

if (!await _currentContext.AccessReports(organizationId))
{
throw new NotFoundException();
Expand All @@ -237,10 +331,6 @@ public async Task<IActionResult> UpdateOrganizationReportDataAsync(Guid organiza
return Ok(response);
}

#endregion

#region ApplicationData Field Endpoints

[HttpGet("{organizationId}/data/application/{reportId}")]
public async Task<IActionResult> GetOrganizationReportApplicationDataAsync(Guid organizationId, Guid reportId)
{
Expand Down Expand Up @@ -297,5 +387,110 @@ public async Task<IActionResult> UpdateOrganizationReportApplicationDataAsync(Gu
}
}

#endregion
[RequireFeature(FeatureFlagKeys.WholeReportDataFileStorage)]
[HttpPost("{organizationId}/{reportId}/file/report-data")]
[SelfHosted(SelfHostedOnly = true)]
[RequestSizeLimit(Constants.FileSize501mb)]
[DisableFormValueModelBinding]
public async Task UploadReportDataAsync(Guid organizationId, Guid reportId, [FromQuery] string reportFileId)
{
await AuthorizeV2Async(organizationId);

if (!Request?.ContentType?.Contains("multipart/") ?? true)
{
throw new BadRequestException("Invalid contenwt.");
Copy link
Contributor

Choose a reason for hiding this comment

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

Typo:

Suggested change
throw new BadRequestException("Invalid contenwt.");
throw new BadRequestException("Invalid content.");

}

if (string.IsNullOrEmpty(reportFileId))
{
throw new BadRequestException("ReportFileId query parameter is required");
}

var report = await _getOrganizationReportQuery.GetOrganizationReportAsync(reportId);
if (report.OrganizationId != organizationId)
{
throw new BadRequestException("Invalid report ID");
}

var fileData = report.GetReportFileData();
if (fileData == null || fileData.Id != reportFileId)
{
throw new NotFoundException();
}

await Request.GetFileAsync(async (stream) =>
{
await _storageService.UploadReportDataAsync(report, fileData, stream);
});

var (valid, length) = await _storageService.ValidateFileAsync(report, fileData, 0, Constants.FileSize501mb);
if (!valid)
{
throw new BadRequestException("File received does not match expected constraints.");
}

fileData.Validated = true;
fileData.Size = length;
report.SetReportFileData(fileData);
report.RevisionDate = DateTime.UtcNow;
await _organizationReportRepo.ReplaceAsync(report);
}

[AllowAnonymous]
[RequireFeature(FeatureFlagKeys.WholeReportDataFileStorage)]
[HttpPost("file/validate/azure")]
public async Task<ObjectResult> AzureValidateFile()
{
return await ApiHelpers.HandleAzureEvents(Request, new Dictionary<string, Func<Azure.Messaging.EventGrid.EventGridEvent, Task>>
{
{
"Microsoft.Storage.BlobCreated", async (eventGridEvent) =>
{
try
{
var blobName =
eventGridEvent.Subject.Split($"{AzureOrganizationReportStorageService.ContainerName}/blobs/")[1];
var reportId = AzureOrganizationReportStorageService.ReportIdFromBlobName(blobName);
var report = await _organizationReportRepo.GetByIdAsync(new Guid(reportId));
if (report == null)
{
if (_storageService is AzureOrganizationReportStorageService azureStorageService)
{
await azureStorageService.DeleteBlobAsync(blobName);
}

return;
}

var fileData = report.GetReportFileData();
if (fileData == null)
{
return;
}

await _validateCommand.ValidateAsync(report, fileData.Id!);
}
catch (Exception e)
{
_logger.LogError(e, "Uncaught exception occurred while handling event grid event: {Event}",
JsonSerializer.Serialize(eventGridEvent));
}
}
}
});
}

private async Task AuthorizeV2Async(Guid organizationId)
{
if (!await _currentContext.AccessReports(organizationId))
{
throw new NotFoundException();
}

var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
if (orgAbility is null || !orgAbility.UseRiskInsights)
{
throw new BadRequestException("Your organization's plan does not support this feature.");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Models.Data;

namespace Bit.Api.Dirt.Models.Response;

Expand All @@ -13,6 +14,7 @@ public class OrganizationReportResponseModel
public int? PasswordCount { get; set; }
public int? PasswordAtRiskCount { get; set; }
public int? MemberCount { get; set; }
public ReportFile? File { get; set; }
public DateTime? CreationDate { get; set; } = null;
public DateTime? RevisionDate { get; set; } = null;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Bit.Core.Enums;

namespace Bit.Api.Dirt.Models.Response;

public class OrganizationReportV2ResponseModel
{
public OrganizationReportV2ResponseModel() { }

public string ReportDataUploadUrl { get; set; } = string.Empty;
public OrganizationReportResponseModel ReportResponse { get; set; } = null!;
public FileUploadType FileUploadType { get; set; }
}
1 change: 1 addition & 0 deletions src/Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ public static class FeatureFlagKeys
public const string ArchiveVaultItems = "pm-19148-innovation-archive";

/* DIRT Team */
public const string WholeReportDataFileStorage = "pm-31920-whole-report-data-file-storage";
public const string EventManagementForDataDogAndCrowdStrike = "event-management-for-datadog-and-crowdstrike";
public const string EventDiagnosticLogging = "pm-27666-siem-event-log-debugging";
public const string EventManagementForHuntress = "event-management-for-huntress";
Expand Down
Loading
Loading