Skip to content

[Custom Reports] Enforce report sharing checks on drill-down, export, clone, delete, and column-config#1950

Open
robertSt7 wants to merge 4 commits into
2025.4from
fix/custom-report-access-control
Open

[Custom Reports] Enforce report sharing checks on drill-down, export, clone, delete, and column-config#1950
robertSt7 wants to merge 4 commits into
2025.4from
fix/custom-report-access-control

Conversation

@robertSt7

Copy link
Copy Markdown
Contributor

Summary

CustomReportRepository::loadByName() resolves a report by name with no ownership
check, while loadByNameForCurrentUser() wraps it and enforces the report's sharing
model (admin flag, shareGlobally, sharedUserIds, sharedRoleIds). Several code
paths called the unprotected method directly, letting any user with only the
generic reports/reports-config permission read or manage private reports they
were never shared:

  • CustomReportService::getDrillDownOptions()POST /bundle/custom-reports/drill-down-options
  • CsvService::generateCsvFile()POST /bundle/custom-reports/export/csv (queues the async export job)
  • CsvCollectionHandler — the async Messenger worker that actually collects the export data; it runs with no HTTP security token, so it resolves the job-owning user via UserResolverInterface/JobRun::getOwnerId() (the same pattern AbstractHandler::validateJobParameters already uses elsewhere)
  • CustomReportConfigService::cloneCustomReport() / deleteCustomReport() — clone/delete config endpoints
  • ColumnService::getColumnConfig() — column-config endpoint

All of these now route through getAllowedReport() (throws ForbiddenException,
synchronous HTTP context) or the new getAllowedReportForUser() (returns null,
used from the async worker), matching the pattern already used correctly by
getChartData() and updateCustomReport().

Changes

  • CustomReportRepository: new loadByNameForUser(string $name, User $user): ?Config
  • CustomReportConfigService: getAllowedReport() made public (was private, reused
    by existing callers unchanged); new getAllowedReportForUser()
  • Swapped the unprotected calls in the five spots above
  • Added REPORT_PERMISSION_MISSING_MESSAGE to the execution-engine Config enum
    plus translations in all 8 locale files, for the async abort path
  • Updated @throws docblocks on the affected interfaces/controllers

Test plan

  • As a user with only the generic reports/reports-config permission and no
    sharing on a given report, confirm drill-down-options, export/csv,
    clone, delete, and column-config all now return 403 (matching the existing
    chart endpoint behavior) instead of returning report data
  • Confirm CSV export still succeeds end-to-end for a report the user is
    allowed to access (request-time check passes, async worker check passes,
    file downloads normally)
  • Run the bundle's CI test suite

🤖 Generated with Claude Code

… clone, delete, and column-config endpoints

loadByName() bypassed the per-report sharing model (shareGlobally, sharedUserIds,
sharedRoleIds) that loadByNameForCurrentUser() enforces. Several code paths used
the unprotected method, letting any user with only the generic reports permission
read or manage private reports they were never shared:

- CustomReportService::getDrillDownOptions()
- CsvService::generateCsvFile() (queues the async CSV export job)
- CsvCollectionHandler (async worker; resolves the job-owning user via
  UserResolverInterface since it runs with no HTTP security token)
- CustomReportConfigService::cloneCustomReport()/deleteCustomReport()
- ColumnService::getColumnConfig()

All now route through getAllowedReport()/getAllowedReportForUser(), matching the
already-correct getChartData() and updateCustomReport().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 5 minor

Results:
5 new issues

Category Results
CodeStyle 5 minor

View in Codacy

🟢 Metrics 4 complexity

Metric Results
Complexity 4

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Enforces custom-report sharing permissions across previously unprotected operations, including asynchronous CSV exports.

Changes:

  • Adds current-user and explicit-user report authorization checks.
  • Secures drill-down, export, clone, delete, and column configuration paths.
  • Adds translated execution-engine permission errors.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
translations/studio.sv.yaml Adds Swedish permission error.
translations/studio.no.yaml Adds Norwegian permission error.
translations/studio.it.yaml Adds Italian permission error.
translations/studio.fr.yaml Adds French permission error.
translations/studio.es.yaml Adds Spanish permission error.
translations/studio.en.yaml Adds English permission error.
translations/studio.de.yaml Adds German permission error.
src/ExecutionEngine/Util/Config.php Defines the report permission message key.
src/Bundle/CustomReport/Service/CustomReportService.php Secures drill-down access.
src/Bundle/CustomReport/Service/CustomReportConfigServiceInterface.php Exposes authorization methods and exceptions.
src/Bundle/CustomReport/Service/CustomReportConfigService.php Secures clone/delete and delegates user checks.
src/Bundle/CustomReport/Service/CsvServiceInterface.php Documents export authorization.
src/Bundle/CustomReport/Service/CsvService.php Checks access before queuing exports.
src/Bundle/CustomReport/Service/ColumnServiceInterface.php Documents authorization failures.
src/Bundle/CustomReport/Service/ColumnService.php Secures column configuration.
src/Bundle/CustomReport/Repository/CustomReportRepositoryInterface.php Adds explicit-user report lookup.
src/Bundle/CustomReport/Repository/CustomReportRepository.php Implements explicit-user sharing checks.
src/Bundle/CustomReport/ExecutionEngine/AutomationAction/Messenger/Handler/CsvCollectionHandler.php Revalidates export access asynchronously.
src/Bundle/CustomReport/Controller/Config/UpdateController.php Documents forbidden responses.
src/Bundle/CustomReport/Controller/Config/DeleteController.php Documents forbidden responses.
src/Bundle/CustomReport/Controller/Config/ColumnConfigController.php Documents forbidden responses.
src/Bundle/CustomReport/Controller/Config/CloneController.php Documents forbidden responses.

Comment thread src/Bundle/CustomReport/Service/CsvServiceInterface.php
Comment thread src/Bundle/CustomReport/Service/CustomReportService.php
robertSt7 and others added 3 commits July 14, 2026 10:00
…on, update @throws docs

- CsvCollectionHandler: keep the report lookup's NotFoundException on its own
  abort path (CSV_DATA_COLLECTION_FAILED_MESSAGE), separate from the explicit
  permission-denied abort, instead of letting it propagate uncaught and cause
  the Messenger message to fail/retry.
- Document NotFoundException alongside ForbiddenException on
  CsvServiceInterface/CsvService::generateCsvFile() and getDrillDownOptions(),
  since getAllowedReport() can throw either. Also fixes the same pre-existing
  gap on getChartData()'s interface docblock.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PHPStan couldn't prove the catch branch's abort() call always terminates,
so it flagged \$reportConfig as possibly undefined at the later null-check.
Pre-initializing to null resolves it and matches the \$user pattern already
used earlier in the same method.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@robertSt7
robertSt7 requested a review from martineiber July 14, 2026 13:01
@robertSt7 robertSt7 self-assigned this Jul 14, 2026
@martineiber martineiber added this to the 2025.4.8 milestone Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants