-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[PM-34909] Serve only validated report files from get-latest and self-hosted file endpoints #7465
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
base: main
Are you sure you want to change the base?
Changes from all commits
091d503
d69fab5
c50e572
8561e00
d5c0bd9
dcd4ec0
d3e0cf9
c38911a
06adae0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -141,19 +141,18 @@ public async Task<IActionResult> GetLatestOrganizationReportAsync(Guid organizat | |
|
|
||
| await AuthorizeAsync(organizationId); | ||
|
|
||
| var latestReport = await _getOrganizationReportQuery.GetLatestOrganizationReportAsync(organizationId); | ||
| var isAccessIntelligenceV2 = _featureService.IsEnabled(FeatureFlagKeys.AccessIntelligenceVersion2); | ||
|
|
||
| if (latestReport == null) | ||
| { | ||
| throw new NotFoundException(); | ||
| } | ||
| var latestReport = isAccessIntelligenceV2 | ||
| ? await _getOrganizationReportQuery.ReadLatestOrganizationReportAsync(organizationId) | ||
| : await _getOrganizationReportQuery.GetLatestOrganizationReportAsync(organizationId); | ||
|
|
||
| var response = new OrganizationReportResponseModel(latestReport); | ||
|
|
||
| if (_featureService.IsEnabled(FeatureFlagKeys.AccessIntelligenceVersion2)) | ||
| if (isAccessIntelligenceV2) | ||
| { | ||
| var fileData = latestReport.GetReportFile(); | ||
| if (fileData is { Validated: true }) | ||
| if (fileData != null) | ||
| { | ||
| response.ReportFileDownloadUrl = await _storageService.GetReportDataDownloadUrlAsync(latestReport, fileData); | ||
| response.FileUploadType = _storageService.FileUploadType; | ||
|
|
@@ -393,7 +392,7 @@ public async Task UploadReportFileAsync(Guid organizationId, Guid reportId, [Fro | |
| } | ||
|
|
||
| var fileData = report.GetReportFile(); | ||
| if (fileData == null || fileData.Id != reportFileId) | ||
| if (fileData == null || fileData.Id != reportFileId || fileData.Validated) | ||
| { | ||
| throw new NotFoundException(); | ||
| } | ||
|
|
@@ -449,6 +448,11 @@ public async Task<IActionResult> DownloadReportFileAsync(Guid organizationId, Gu | |
| throw new NotFoundException(); | ||
| } | ||
|
|
||
| if (_featureService.IsEnabled(FeatureFlagKeys.AccessIntelligenceVersion2) && !fileData.Validated) | ||
|
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.
Contributor
Author
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. We look the report row up by report id via GetAuthorizedReportAsync which doesn't filter by validated (only the new ReadLatestByOrganizationIdAsync in the repo layer does, and that's the latest-by-org path, not by-id). The route (DownloadReportFileAsync) also has no [RequireFeature] attribute, just [SelfHosted(SelfHostedOnly = true)]. So with the flag ON, this guard is the only block against streaming an unvalidated file. Guard shipped in 091d50362. |
||
| { | ||
| throw new NotFoundException(); | ||
| } | ||
|
|
||
| var stream = await _storageService.GetReportReadStreamAsync(report, fileData); | ||
| if (stream == null) | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,7 +27,31 @@ public async Task<OrganizationReport> GetLatestByOrganizationIdAsync(Guid organi | |
| { | ||
| var dbContext = GetDatabaseContext(scope); | ||
| var result = await dbContext.OrganizationReports | ||
| .Where(p => p.OrganizationId == organizationId) | ||
| .Where(p => p.OrganizationId == organizationId | ||
| && p.ReportData != string.Empty) | ||
| .OrderByDescending(p => p.RevisionDate) | ||
| .Take(1) | ||
| .FirstOrDefaultAsync(); | ||
|
|
||
| if (result == null) return default; | ||
|
|
||
| return Mapper.Map<OrganizationReport>(result); | ||
| } | ||
| } | ||
|
|
||
| public async Task<OrganizationReport> ReadLatestByOrganizationIdAsync(Guid organizationId) | ||
| { | ||
| using (var scope = ServiceScopeFactory.CreateScope()) | ||
| { | ||
| var dbContext = GetDatabaseContext(scope); | ||
| // Substring match is coupled to SetReportFile (OrganizationReport.cs) writing via | ||
| // JsonHelpers.IgnoreWritingNull (PascalCase, no whitespace). The Dapper/MSSQL path | ||
| // uses JSON_VALUE which is format-agnostic; changing the serializer options would | ||
| // silently return zero rows here without affecting the MSSQL path. | ||
| var result = await dbContext.OrganizationReports | ||
| .Where(p => p.OrganizationId == organizationId | ||
| && p.ReportFile != null | ||
| && p.ReportFile.Contains("\"Validated\":true")) | ||
|
Comment on lines
+52
to
+54
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. π¨ SUGGESTED: EF filter relies on a raw JSON substring match, which drifts from the Dapper Details and fixThe Dapper/MSSQL stored procedure filters with Consider using
Contributor
Author
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. The substring match is our repo's convention for these cases currently. Introducing this new json extract convention is out of scope for this pr but I still made a comment here 8561e00 |
||
| .OrderByDescending(p => p.RevisionDate) | ||
| .Take(1) | ||
| .FirstOrDefaultAsync(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| CREATE PROCEDURE [dbo].[OrganizationReport_ReadLatestByOrganizationId] | ||
|
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. Since
Contributor
Author
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.
Yes that's the plan, once |
||
| @OrganizationId UNIQUEIDENTIFIER | ||
| AS | ||
| BEGIN | ||
| SET NOCOUNT ON | ||
|
|
||
| SELECT TOP 1 | ||
| * | ||
| FROM | ||
| [dbo].[OrganizationReportView] | ||
| WHERE | ||
| [OrganizationId] = @OrganizationId | ||
| AND JSON_VALUE([ReportFile], '$.Validated') = 'true' | ||
| ORDER BY | ||
| [RevisionDate] DESC | ||
| END | ||
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.
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.
Null check moved down a layer. Both GetLatestOrganizationReportAsync and ReadLatestOrganizationReportAsync in GetOrganizationReportQuery (.../Reports/ReportFeatures/GetOrganizationReportQuery.cs) throw NotFoundException on null, covered by _NullResult_ThrowsNotFoundException tests for each in GetOrganizationReportQueryTests.cs. So the controller never sees a null latestReport. Shipped in c50e572d8.