Skip to content

PM-31920 adding the whole report endpoints v2#7079

Open
prograhamming wants to merge 27 commits intomainfrom
dirt/pm-31920-whole-report-data-v2-endpoints-access-intelligence
Open

PM-31920 adding the whole report endpoints v2#7079
prograhamming wants to merge 27 commits intomainfrom
dirt/pm-31920-whole-report-data-v2-endpoints-access-intelligence

Conversation

@prograhamming
Copy link
Contributor

@prograhamming prograhamming commented Feb 25, 2026

🎟️ Tracking

This is a PR for a sub-task (PM-32520) on user story PM-31923

📔 Objective

Creating new V2 endpoints for read and update operations on the whole report in the database. This will also include the logic for saving a reportData file in Azure Blob storage and server if self-hosted.

📸 Screenshots

@github-actions
Copy link
Contributor

github-actions bot commented Feb 25, 2026

Logo
Checkmarx One – Scan Summary & Detailscb3e38c7-6734-4a74-9b88-e1e011355cfb


New Issues (2) Checkmarx found the following issues in this Pull Request
# Severity Issue Source File / Package Checkmarx Insight
1 MEDIUM CSRF /src/Api/Dirt/Controllers/OrganizationReportsController.cs: 507
detailsMethod at line 507 of /src/Api/Dirt/Controllers/OrganizationReportsController.cs gets a parameter from a user request from request. This paramet...
Attack Vector
2 MEDIUM CSRF /src/Api/Auth/Controllers/AccountsController.cs: 291
detailsMethod at line 291 of /src/Api/Auth/Controllers/AccountsController.cs gets a parameter from a user request from model. This parameter value flow...
Attack Vector

Fixed Issues (2) Great job! The following issues were fixed in this Pull Request
Severity Issue Source File / Package
MEDIUM CSRF /src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs: 146
MEDIUM CSRF /src/Api/Dirt/Controllers/OrganizationReportsController.cs: 233

prograhamming and others added 3 commits February 28, 2026 22:19
…elligence' of github.com:bitwarden/server into dirt/pm-31920-whole-report-data-v2-endpoints-access-intelligence
@codecov
Copy link

codecov bot commented Mar 4, 2026

Codecov Report

❌ Patch coverage is 0.52356% with 380 lines in your changes missing coverage. Please review.
✅ Project coverage is 13.87%. Comparing base (8d9ace6) to head (adaf501).

Files with missing lines Patch % Lines
...eportFeatures/UpdateOrganizationReportV2Command.cs 0.00% 97 Missing ⚠️
.../Services/AzureOrganizationReportStorageService.cs 0.00% 74 Missing ⚠️
.../ReportFeatures/CreateOrganizationReportCommand.cs 0.00% 64 Missing ⚠️
.../Services/LocalOrganizationReportStorageService.cs 0.00% 59 Missing ⚠️
...tFeatures/ValidateOrganizationReportFileCommand.cs 0.00% 30 Missing ⚠️
...portFeatures/Requests/OrganizationReportMetrics.cs 0.00% 12 Missing ⚠️
src/Core/Dirt/Entities/OrganizationReport.cs 0.00% 9 Missing ⚠️
...tures/GetOrganizationReportApplicationDataQuery.cs 0.00% 9 Missing ⚠️
...ures/Requests/UpdateOrganizationReportV2Request.cs 0.00% 9 Missing ⚠️
...s/Services/NoopOrganizationReportStorageService.cs 0.00% 6 Missing ⚠️
... and 8 more
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #7079       +/-   ##
===========================================
- Coverage   56.88%   13.87%   -43.02%     
===========================================
  Files        2028     1248      -780     
  Lines       88826    53222    -35604     
  Branches     7918     4113     -3805     
===========================================
- Hits        50528     7384    -43144     
- Misses      36468    45706     +9238     
+ Partials     1830      132     -1698     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@prograhamming prograhamming added the ai-review Request a Claude code review label Mar 4, 2026
@prograhamming prograhamming marked this pull request as ready for review March 4, 2026 21:10
@prograhamming prograhamming requested a review from a team as a code owner March 4, 2026 21:10
@prograhamming prograhamming marked this pull request as draft March 4, 2026 21:11
@prograhamming
Copy link
Contributor Author

@claude review this pr

@claude
Copy link
Contributor

claude bot commented Mar 4, 2026

Claude finished @prograhamming's task in 3m 15s —— View job


Code Review: PM-31920 V2 Report Endpoints

  • Gather context and read changed files
  • Review controller changes
  • Review core domain/entity changes
  • Review storage services
  • Review commands and queries
  • Review tests
  • Post findings

I reviewed all 39 changed files in this PR. Many issues from previous review rounds have been addressed (null checks, path traversal guards, typos, FileSize on update request, old file cleanup). Here are the remaining findings on the current state of the code:


Security

1. ReportFile internal data exposed in API response (Low)
OrganizationReportResponseModel.cs:14 — The ReportFile object (containing Id, Size, Validated, FileName) is serialized directly to API consumers. The Id is a CoreHelpers.SecureRandomString used as a blob storage path component. While the download URL is already provided via ReportFileDownloadUrl, leaking the internal file ID gives clients knowledge of storage paths. Consider either removing this property (since ReportFileDownloadUrl serves the download use case) or creating a DTO that only exposes FileName and Size.

2. ValidateFileAsync has side effects beyond validation (Low)
AzureOrganizationReportStorageService.cs:57-87 — This method sets blob metadata (organizationId) and HTTP headers (Content-Disposition) in addition to validating size constraints. The name suggests a read-only operation. Consider renaming to ValidateAndFinalizeFileAsync to signal the side effects, or extracting the finalization logic.


Bugs / Correctness

3. ValidateOrganizationReportFileCommand deletes the entire report on failed Azure validation
ValidateOrganizationReportFileCommand.cs:38-40 — When Azure EventGrid triggers validation and the file size is invalid (e.g., an upload of 0 bytes due to a transient failure), the command deletes both the blob and the entire OrganizationReport database record. This means a flaky upload or Azure storage hiccup could permanently destroy a report that still has valid metadata, summary data, and application data. Consider only deleting the blob and marking the file as invalid (setting Validated = false and clearing the ReportFile JSON) rather than deleting the entire report entity.

4. V1 GetOrganizationReportAsync returns raw entity
OrganizationReportsController.cs:229 — The V1 path returns Ok(v1Report) (the raw OrganizationReport entity), while V2 returns Ok(new OrganizationReportResponseModel(report)). This means V1 serializes the entity directly (including ReportFile as a raw JSON string, not the deserialized object). This is pre-existing behavior but creates an inconsistent API response shape between the two code paths. Not blocking, but worth noting.


Design

5. OrganizationId and ReportId on UpdateOrganizationReportV2Request silently overwritten
OrganizationReportsController.cs:244-245 — The V2 update path overwrites request.OrganizationId and request.ReportId from route params, making client-sent body values irrelevant. This is actually more secure than V1 (which rejects mismatches), but the request model still allows these fields in the body, creating a confusing API contract. Consider adding [JsonIgnore] to these properties to make it explicit they come from the route. Not blocking.

6. V2 update FileSize validation allows 0 when RequiresNewFileUpload is true
OrganizationReportsController.cs:249-257 — The controller checks request.FileSize.HasValue and request.FileSize.Value > Constants.FileSize501mb, but doesn't reject FileSize = 0 or negative values. A client could send FileSize = 0, which would pass validation but result in a ReportFile.Size of 0 and a leeway check of [0, 1MB] in the self-hosted upload path. For Azure this is handled by EventGrid validation, but for self-hosted it means only files up to 1MB would be accepted despite the 501MB limit. Consider adding FileSize <= 0 as a validation error.


Test Coverage

7. No controller tests for UploadReportFileAsync or AzureValidateFile
The self-hosted file upload endpoint (UploadReportFileAsync) and Azure EventGrid callback (AzureValidateFile) have no controller-level tests. These are the most security-sensitive endpoints (anonymous access on Azure validate, multipart file handling on upload). While the underlying commands/services are tested, the controller logic (authorization, content-type check, reportFileId matching, leeway validation) is not covered.

8. UpdateOrganizationReportV2Command test for RequiresNewFileUpload doesn't verify old file cleanup
UpdateOrganizationReportV2CommandTests.cs:121-158 — The test UpdateAsync_WithRequiresNewFileUpload_CreatesNewReportFile doesn't set up an existing ReportFile on the report, so it never exercises the DeleteReportFilesAsync call added at line 97-101 of the command. Consider adding a test that has an existing file and verifies DeleteReportFilesAsync is called with the old file ID.


Summary

The PR is in good shape overall. The path traversal guard, null checks, FileSize on update, and old file cleanup have all been addressed from previous review rounds. The remaining items are mostly low-severity design/naming concerns and test coverage gaps. The most actionable items are:

  • Item 3 (report deletion on validation failure) warrants discussion — it's a destructive operation triggered by potentially transient failures
  • Item 6 (FileSize = 0 validation gap) could cause unexpected upload failures on self-hosted
  • Item 8 (missing test for old file cleanup) should be straightforward to add

Comment on lines +91 to +101
if (request.RequiresNewFileUpload)
{
var fileData = new ReportFile
{
Id = CoreHelpers.SecureRandomString(32, upper: false, special: false),
FileName = "report-data.json",
Validated = false,
Size = 0
};
existingReport.SetReportFile(fileData);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: When RequiresNewFileUpload is true, the new ReportFile is created with Size = 0. But the controller's UploadReportFileAsync (line 349) uses a 1MB leeway around fileData.Size for validation: Math.Max(0, fileData.Size - leeway) which for Size = 0 means minimum = 0.

For CREATE this works because AddOrganizationReportRequest.FileSize is used. But for UPDATE, the UpdateOrganizationReportV2Request has no FileSize property, so the size is always 0 and the leeway check effectively becomes 0 <= length <= 501MB. This means no meaningful file size validation occurs for update uploads.

Should UpdateOrganizationReportV2Request also carry a FileSize field when RequiresNewFileUpload is true, so the upload validation is meaningful?

Comment on lines +66 to +76
var blobProperties = await blobClient.GetPropertiesAsync();
var metadata = blobProperties.Value.Metadata;
metadata["organizationId"] = report.OrganizationId.ToString();
await blobClient.SetMetadataAsync(metadata);

var headers = new BlobHttpHeaders
{
ContentDisposition = $"attachment; filename=\"{fileData.FileName}\""
};
await blobClient.SetHttpHeadersAsync(headers);

Copy link
Contributor

Choose a reason for hiding this comment

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

Side effect in validation: ValidateFileAsync not only validates the blob but also sets metadata (organizationId) and HTTP headers (Content-Disposition) on it. This is unexpected for a method named "Validate". Consider either:

  1. Renaming to something like ValidateAndFinalizeFileAsync to clarify the side effects, or
  2. Extracting the metadata/header setting into a separate method called during finalization

Comment on lines 22 to 41
@@ -32,6 +35,7 @@ public OrganizationReportResponseModel(OrganizationReport organizationReport)
PasswordCount = organizationReport.PasswordCount;
PasswordAtRiskCount = organizationReport.PasswordAtRiskCount;
MemberCount = organizationReport.MemberCount;
ReportFile = organizationReport.GetReportFile();
CreationDate = organizationReport.CreationDate;
RevisionDate = organizationReport.RevisionDate;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: The response model only maps a subset of the entity's metric fields (PasswordCount, PasswordAtRiskCount, MemberCount). The entity has 12 metric fields (application, member, password, each with total/atRisk/critical/criticalAtRisk). If the client needs these metrics, the response model should map all of them. If they're intentionally excluded from the API response, a comment explaining why would help future maintainers.

Comment on lines +332 to +333
var report = await _getOrganizationReportQuery.GetOrganizationReportAsync(reportId);
if (report.OrganizationId != organizationId)
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug (High): Same null dereference pattern as already flagged in GetOrganizationReportAsync at line 191. If GetOrganizationReportAsync(reportId) returns null, accessing report.OrganizationId on line 333 will throw a NullReferenceException.

Suggested change
var report = await _getOrganizationReportQuery.GetOrganizationReportAsync(reportId);
if (report.OrganizationId != organizationId)
var report = await _getOrganizationReportQuery.GetOrganizationReportAsync(reportId);
if (report == null)
{
throw new NotFoundException();
}
if (report.OrganizationId != organizationId)

Comment on lines +48 to +57
public Task DeleteReportFilesAsync(OrganizationReport report, string reportFileId)
{
var dirPath = Path.Combine(_baseDirPath, report.OrganizationId.ToString(),
report.CreationDate.ToString("MM-dd-yyyy"), report.Id.ToString(), reportFileId);
if (Directory.Exists(dirPath))
{
Directory.Delete(dirPath, true);
}
return Task.CompletedTask;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Security (Medium): The EnsurePathWithinBaseDir guard was correctly added to WriteFileAsync and ValidateFileAsync, but DeleteReportFilesAsync still constructs a path from reportFileId via Path.Combine and passes it to Directory.Delete(dirPath, true) without the guard. For consistency with the defense-in-depth approach applied to the other methods, add the same check here:

Suggested change
public Task DeleteReportFilesAsync(OrganizationReport report, string reportFileId)
{
var dirPath = Path.Combine(_baseDirPath, report.OrganizationId.ToString(),
report.CreationDate.ToString("MM-dd-yyyy"), report.Id.ToString(), reportFileId);
if (Directory.Exists(dirPath))
{
Directory.Delete(dirPath, true);
}
return Task.CompletedTask;
}
public Task DeleteReportFilesAsync(OrganizationReport report, string reportFileId)
{
var dirPath = Path.Combine(_baseDirPath, report.OrganizationId.ToString(),
report.CreationDate.ToString("MM-dd-yyyy"), report.Id.ToString(), reportFileId);
EnsurePathWithinBaseDir(dirPath);
if (Directory.Exists(dirPath))
{
Directory.Delete(dirPath, true);
}
return Task.CompletedTask;
}

Id = CoreHelpers.SecureRandomString(32, upper: false, special: false),
FileName = "report-data.json",
Validated = false,
Size = 0
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ IMPORTANT (Bug): Size = 0 here means the self-hosted UploadReportFileAsync controller method will compute a validation range of [0, 1MB] (since minimum = Max(0, 0 - 1MB) = 0 and maximum = Min(0 + 1MB, 501MB) = 1MB). Any file larger than 1MB uploaded through the update path on self-hosted instances will fail validation with "File received does not match expected constraints."

The create path works correctly because AddOrganizationReportRequest includes a FileSize property that the client provides, but UpdateOrganizationReportV2Request has no equivalent field.

To fix, add a FileSize property to UpdateOrganizationReportV2Request, validate it in the controller (similar to the create path at lines 143-151 of the controller), and use it here:

Suggested change
Size = 0
Size = request.FileSize ?? 0

This was flagged in a previous review and remains unfixed.

Id = CoreHelpers.SecureRandomString(32, upper: false, special: false),
FileName = "report-data.json",
Size = request.FileSize ?? 0,
Validated = false
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ IMPORTANT (Security): Both this command and UpdateOrganizationReportV2Command correctly set Validated = false for new files. However, the ReportFile class (src/Core/Dirt/Models/Data/ReportFile.cs line 29) declares the default as public bool Validated { get; set; } = true;.

This is a fail-open default for a security-sensitive field. If System.Text.Json deserialization ever produces a ReportFile instance without an explicit Validated value (corrupted JSON, a field missing from the serialized data, etc.), the file would be treated as validated, and the controller would generate a download URL for a potentially unverified file.

The safe default should be false (fail-closed). Since both creation paths already set Validated = false explicitly, and the validation paths set it to true explicitly, changing the default in ReportFile.cs to false will not affect normal code flow but will protect against edge cases.

Comment on lines +91 to +101
if (request.RequiresNewFileUpload)
{
var fileData = new ReportFile
{
Id = CoreHelpers.SecureRandomString(32, upper: false, special: false),
FileName = "report-data.json",
Validated = false,
Size = 0
};
existingReport.SetReportFile(fileData);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug (Medium): When RequiresNewFileUpload is true, the old file blob/directory is never cleaned up. The existing ReportFile (with its old Id) is overwritten by SetReportFile(fileData) with a new ID, but no call is made to _storageService.DeleteReportFilesAsync() to remove the previous file from storage.

This will cause orphaned blobs/files to accumulate over time. The command doesn't inject IOrganizationReportStorageService, so it can't perform cleanup.

Consider either:

  1. Injecting IOrganizationReportStorageService and deleting the old file before replacing, or
  2. Documenting that cleanup happens elsewhere (e.g., a background job)
Suggested change
if (request.RequiresNewFileUpload)
{
var fileData = new ReportFile
{
Id = CoreHelpers.SecureRandomString(32, upper: false, special: false),
FileName = "report-data.json",
Validated = false,
Size = 0
};
existingReport.SetReportFile(fileData);
}
if (request.RequiresNewFileUpload)
{
// TODO: Clean up old file from storage before replacing
// var oldFileData = existingReport.GetReportFile();
// if (oldFileData?.Id != null)
// {
// await _storageService.DeleteReportFilesAsync(existingReport, oldFileData.Id);
// }
var fileData = new ReportFile
{
Id = CoreHelpers.SecureRandomString(32, upper: false, special: false),
FileName = "report-data.json",
Validated = false,
Size = 0
};
existingReport.SetReportFile(fileData);
}

PasswordCount = organizationReport.PasswordCount;
PasswordAtRiskCount = organizationReport.PasswordAtRiskCount;
MemberCount = organizationReport.MemberCount;
ReportFile = organizationReport.GetReportFile();
Copy link
Contributor

Choose a reason for hiding this comment

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

Information leak: The ReportFile object (containing internal fields like Id, Size, Validated, FileName) is exposed directly to API consumers via the response model. The Id is a CoreHelpers.SecureRandomString that's used as a path component for blob storage — leaking it gives clients knowledge of internal storage paths.

Consider either removing this property from the response model (the download URL is already exposed via ReportFileDownloadUrl), or creating a slimmed-down DTO that only exposes what the client needs (e.g., FileName and Size).

Comment on lines +358 to +362

var leeway = 1024L * 1024L; // 1 MB
var minimum = Math.Max(0, fileData.Size - leeway);
var maximum = Math.Min(fileData.Size + leeway, Constants.FileSize501mb);
var (valid, length) = await _storageService.ValidateFileAsync(report, fileData, minimum, maximum);
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug (Medium): File size validation is ineffective for update-triggered uploads. When RequiresNewFileUpload is true via the update path, UpdateOrganizationReportV2Command creates a new ReportFile with Size = 0 (line 98 of that command). Here, the leeway calculation becomes:

  • minimum = Math.Max(0, 0 - 1MB) = 0
  • maximum = Math.Min(0 + 1MB, 501MB) = 1MB

This means uploaded files from the update path are silently limited to ~1MB instead of 501MB. For the create path this works correctly because AddOrganizationReportRequest.FileSize is set by the client.

UpdateOrganizationReportV2Request should also carry a FileSize field when RequiresNewFileUpload is true, so the validation boundary is meaningful.

Comment on lines +232 to +246
// UPDATE Whole Report
[HttpPatch("{organizationId}/{reportId}")]
public async Task<IActionResult> UpdateOrganizationReportAsync(Guid organizationId, [FromBody] UpdateOrganizationReportRequest request)
[RequestSizeLimit(Constants.FileSize501mb)]
public async Task<IActionResult> UpdateOrganizationReportAsync(
Guid organizationId,
Guid reportId,
[FromBody] UpdateOrganizationReportV2Request request)
{
if (_featureService.IsEnabled(FeatureFlagKeys.AccessIntelligenceVersion2))
{
await AuthorizeAsync(organizationId);

request.OrganizationId = organizationId;
request.ReportId = reportId;

Copy link
Contributor

Choose a reason for hiding this comment

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

Design: In the V2 path, the controller overwrites request.OrganizationId and request.ReportId from route params (lines 244-245), making the client-sent values in the body irrelevant. This is correct from a security standpoint (prevents ID spoofing), but in the V1 path (line 268), the controller validates that request.OrganizationId matches the route param and rejects mismatches.

This means a client sending { "organizationId": "wrong-id" } will succeed in V2 (silently overwritten) but fail in V1 (rejected). Consider adding [JsonIgnore] to OrganizationId and ReportId on UpdateOrganizationReportV2Request to make the API contract explicit that these come from the route.

Comment on lines +57 to +87
public async Task<(bool valid, long length)> ValidateFileAsync(
OrganizationReport report, ReportFile fileData, long minimum, long maximum)
{
await InitAsync();

var blobClient = _containerClient!.GetBlobClient(BlobPath(report, fileData.Id!, fileData.FileName));

try
{
var blobProperties = await blobClient.GetPropertiesAsync();
var metadata = blobProperties.Value.Metadata;
metadata["organizationId"] = report.OrganizationId.ToString();
await blobClient.SetMetadataAsync(metadata);

var headers = new BlobHttpHeaders
{
ContentDisposition = $"attachment; filename=\"{fileData.FileName}\""
};
await blobClient.SetHttpHeadersAsync(headers);

var length = blobProperties.Value.ContentLength;
var valid = minimum <= length && length <= maximum;

return (valid, length);
}
catch (Exception ex)
{
_logger.LogError(ex, "A storage operation failed in {MethodName}", nameof(ValidateFileAsync));
return (false, -1);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Naming / side effects: ValidateFileAsync does more than validation — it also sets blob metadata (organizationId) and HTTP headers (Content-Disposition). This makes it surprising for callers who only expect a boolean + length result. Consider renaming to ValidateAndFinalizeFileAsync, or extracting the metadata/header logic into a separate method called during finalization.

Comment on lines +380 to +416
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.GetReportFile();
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));
}
}
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: In the AzureValidateFile endpoint, the catch block at line 409 logs the serialized eventGridEvent. Verify that event data cannot contain PII or sensitive blob content. The current event only contains the blob subject path (org ID, date, report ID, file ID), which appears safe. But since Bitwarden policy is to never log PII, worth confirming that no Event Grid subscription custom data could leak into this path.

Also, consider limiting the scope of the try/catch. Currently, if _organizationReportRepo.GetByIdAsync or DeleteBlobAsync throw, the exception is swallowed and logged. This could mask issues like database connectivity problems.

Comment on lines 42 to 82
@@ -36,8 +50,15 @@ public OrganizationReportsController(
IGetOrganizationReportDataQuery getOrganizationReportDataQuery,
IUpdateOrganizationReportDataCommand updateOrganizationReportDataCommand,
IGetOrganizationReportApplicationDataQuery getOrganizationReportApplicationDataQuery,
IUpdateOrganizationReportApplicationDataCommand updateOrganizationReportApplicationDataCommand
)
IUpdateOrganizationReportApplicationDataCommand updateOrganizationReportApplicationDataCommand,
IFeatureService featureService,
IApplicationCacheService applicationCacheService,
IOrganizationReportStorageService storageService,
ICreateOrganizationReportCommand createReportCommand,
IOrganizationReportRepository organizationReportRepo,
IUpdateOrganizationReportV2Command updateReportV2Command,
IValidateOrganizationReportFileCommand validateCommand,
ILogger<OrganizationReportsController> logger)
{
_currentContext = currentContext;
_getOrganizationReportQuery = getOrganizationReportQuery;
@@ -50,68 +71,195 @@ IUpdateOrganizationReportApplicationDataCommand updateOrganizationReportApplicat
_updateOrganizationReportDataCommand = updateOrganizationReportDataCommand;
_getOrganizationReportApplicationDataQuery = getOrganizationReportApplicationDataQuery;
_updateOrganizationReportApplicationDataCommand = updateOrganizationReportApplicationDataCommand;
_featureService = featureService;
_applicationCacheService = applicationCacheService;
_storageService = storageService;
_createReportCommand = createReportCommand;
_organizationReportRepo = organizationReportRepo;
_updateReportV2Command = updateReportV2Command;
_validateCommand = validateCommand;
_logger = logger;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: This controller has 17 constructor parameters. While this is understandable given the V1/V2 coexistence, it's worth considering whether V2-specific dependencies could be extracted into a separate controller (e.g., OrganizationReportsV2Controller) behind the [RequireFeature] attribute. This would also simplify the feature-flag branching throughout the action methods.

Not a blocking issue — just something to consider for maintainability once the V1 paths are removed.

@prograhamming prograhamming requested a review from a team as a code owner March 6, 2026 22:14
@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 6, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant