-
Notifications
You must be signed in to change notification settings - Fork 1.5k
PM-31920 adding the whole report endpoints v2 #7079
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
prograhamming
wants to merge
11
commits into
main
Choose a base branch
from
dirt/pm-31920-whole-report-data-v2-endpoints-access-intelligence
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,849
−84
Draft
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
99bc641
pm-31920 adding the whole report endpoints v2
prograhamming 8002621
Merge branch 'main' into dirt/pm-31920-whole-report-data-v2-endpoints…
prograhamming d9b1bb7
Merge branch 'main' into dirt/pm-31920-whole-report-data-v2-endpoints…
prograhamming 5d3f7b7
Merge branch 'main' into dirt/pm-31920-whole-report-data-v2-endpoints…
prograhamming 65092cb
pm-31920 changing approach to match others in codebase
prograhamming b6def67
Merge branch 'dirt/pm-31920-whole-report-data-v2-endpoints-access-int…
prograhamming ffa2e80
Merge branch 'main' into dirt/pm-31920-whole-report-data-v2-endpoints…
prograhamming 5852cba
31920 updating code to now use the ReportFile field
prograhamming 0abca22
Merge branch 'dirt/pm-31920-whole-report-data-v2-endpoints-access-int…
prograhamming ed8116d
Merge branch 'main' into dirt/pm-31920-whole-report-data-v2-endpoints…
prograhamming a41cbf5
Merge branch 'main' into dirt/pm-31920-whole-report-data-v2-endpoints…
prograhamming File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||||||
|
|
||||||
|
|
@@ -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; | ||||||
| private readonly IOrganizationReportRepository _organizationReportRepo; | ||||||
| private readonly IValidateOrganizationReportFileCommand _validateCommand; | ||||||
| private readonly ILogger<OrganizationReportsController> _logger; | ||||||
|
|
||||||
| public OrganizationReportsController( | ||||||
| ICurrentContext currentContext, | ||||||
|
|
@@ -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; | ||||||
|
|
@@ -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) | ||||||
| { | ||||||
|
|
@@ -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(); | ||||||
|
|
@@ -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); | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -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) | ||||||
|
|
@@ -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) | ||||||
|
|
@@ -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(); | ||||||
|
|
@@ -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) | ||||||
| { | ||||||
|
|
@@ -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."); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo:
Suggested change
|
||||||
| } | ||||||
|
|
||||||
| 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."); | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
src/Api/Dirt/Models/Response/OrganizationReportV2ResponseModel.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug:
_getDataV2Queryis injected but never called anywhere in this controller or the codebase. TheGetOrganizationReportDataAsyncGET 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
WholeReportDataFileStoragebranch that calls_getDataV2Query.GetOrganizationReportDataAsync()(similar to how create/update endpoints have V2 branches)?