feat(api-token): scope-annotate read endpoints + OpenAPI security (ADR-021 Phase 4)#559
Conversation
…R-021 Phase 4)
Open the read data API to tokens, fail-closed by default. 21 read endpoints now
declare #[RequireScope('resource:read')] — customers/projects/users/teams/presets/
contracts/ticketsystems, the interpretation/reporting group, entry-by-id, and the
exports — each at its matching resource scope. Admin (ROLE_ADMIN) endpoints keep
their role gate; the scope composes with it (both must pass). Holidays, admin
status and the jira-sync trigger are left unannotated (not token-facing).
OpenAPI (public/api.yml): add the `bearerAuth` (+ `cookieAuth`) securityScheme,
a top-level `security`, and the token/scope model in the description. Per-operation
scope tags are deliberately not duplicated — the code's #[RequireScope] is the
single source of truth, so the two can't drift.
Coverage: RequireScopeCoverageTest asserts every declared scope is a valid
ApiScope and guards the count; ApiTokenAuthTest gains end-to-end checks that a
Phase-4 endpoint is reachable with its scope and denied with a wrong one, and the
fail-closed example moves to a still-unannotated endpoint.
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
There was a problem hiding this comment.
Code Review
This pull request implements Phase 4 of ADR-021 (API Token Authentication) by annotating read endpoints with the #[RequireScope] attribute, updating the OpenAPI specification with bearer security schemes and scope documentation, and adding integration and coverage tests. Feedback on the changes suggests making the regular expression in the new RequireScopeCoverageTest more robust to handle different formatting styles of PHP attributes.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #559 +/- ##
=========================================
Coverage 84.75% 84.75%
Complexity 3286 3286
=========================================
Files 229 229
Lines 8934 8934
=========================================
Hits 7572 7572
Misses 1362 1362
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR continues ADR-021 Phase 4 by formally opening selected read-only API endpoints to personal access tokens via server-side #[RequireScope('…:read')] annotations, while keeping the token firewall fail-closed by default and documenting the auth model in the OpenAPI spec.
Changes:
- Annotates additional read endpoints with
#[RequireScope('resource:read')](including reporting + interpretation endpoints, and/tracking/entry/{id}). - Extends test coverage with a controller-scan guard (
RequireScopeCoverageTest) and Phase-4 token/scope authorization checks (ApiTokenAuthTest). - Updates
public/api.ymland ADR-021 documentation to describe Bearer tokens + scope model.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Security/RequireScopeCoverageTest.php | Adds a scan-based guard to validate #[RequireScope] strings against ApiScope and enforce a minimum annotation count. |
| tests/Controller/ApiTokenAuthTest.php | Adds Phase-4 e2e checks for scope allow/deny behavior and updates the fail-closed example endpoint. |
| src/Controller/Tracking/GetEntryAction.php | Requires entries:read scope for token access to single-entry reads. |
| src/Controller/Interpretation/GroupByWorktimeAction.php | Requires reporting:read for interpretation aggregation endpoint (token-gated). |
| src/Controller/Interpretation/GroupByUserAction.php | Requires reporting:read for interpretation aggregation endpoint (token-gated). |
| src/Controller/Interpretation/GroupByTicketAction.php | Requires reporting:read for interpretation aggregation endpoint (token-gated). |
| src/Controller/Interpretation/GroupByProjectAction.php | Requires reporting:read for interpretation aggregation endpoint (token-gated). |
| src/Controller/Interpretation/GroupByCustomerAction.php | Requires reporting:read for interpretation aggregation endpoint (token-gated). |
| src/Controller/Interpretation/GroupByActivityAction.php | Requires reporting:read for interpretation aggregation endpoint (token-gated). |
| src/Controller/Interpretation/GetLastEntriesAction.php | Requires reporting:read for interpretation last-entries endpoint (token-gated). |
| src/Controller/Default/GetUsersAction.php | Requires users:read for token access to users listing. |
| src/Controller/Default/GetTimeSummaryAction.php | Requires reporting:read for time summary reads via tokens. |
| src/Controller/Default/GetTicketTimeSummaryAction.php | Requires reporting:read for ticket time summary reads via tokens. |
| src/Controller/Default/GetProjectsAction.php | Requires projects:read for token access to project reads. |
| src/Controller/Default/GetContractHoursAction.php | Requires contracts:read for token access to contract-hours reads. |
| src/Controller/Default/ExportCsvAction.php | Requires reporting:read for CSV export reads via tokens. |
| src/Controller/Controlling/ExportAction.php | Requires reporting:read in addition to admin role for controlling export reads via tokens. |
| src/Controller/Admin/GetUsersAction.php | Adds users:read scope requirement atop admin role for admin users endpoint. |
| src/Controller/Admin/GetTicketSystemsAction.php | Adds ticketsystems:read scope requirement atop admin role for ticket systems endpoint. |
| src/Controller/Admin/GetTeamsAction.php | Adds teams:read scope requirement atop admin role for teams endpoint. |
| src/Controller/Admin/GetPresetsAction.php | Adds presets:read scope requirement to presets list endpoint. |
| src/Controller/Admin/GetCustomersAction.php | Adds customers:read scope requirement atop admin role for customers endpoint. |
| src/Controller/Admin/GetContractsAction.php | Adds contracts:read scope requirement atop admin role for contracts endpoint. |
| public/api.yml | Documents Bearer/cookie schemes and adds global security requirements for auth. |
| docs/adr/ADR-021-api-token-authentication.md | Marks Phase 4 as done and documents scope annotation + OpenAPI changes. |
- RequireScopeCoverageTest reads #[RequireScope] via reflection instead of a regex (robust to quoting/grouping/formatting), reports the FQCN on failure, and raises the floor to 27 (the actual count). - OpenAPI: the public operations (/login, /status/check, /status/page) override the global security with `security: []`, so the spec no longer marks them as requiring authentication. Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
|



What
Opens the read data API to personal access tokens, fail-closed by default.
21 read endpoints now declare
#[RequireScope('resource:read')]:customers:read/getAllCustomersprojects:read/getProjectsusers:read/getUsers,/getAllUsersteams:read/getAllTeamspresets:read/getAllPresetscontracts:read/getContracts,/getContractHoursticketsystems:read/getTicketSystemsentries:read/tracking/entry/{id}reporting:read/getTimeSummary,/getTicketTimeSummary/{ticket}, the/interpretation/*group,/controlling/export,/export/{days}Admin (
ROLE_ADMIN) endpoints keep their role gate —#[RequireScope]composes with it (a token needs both an admin user and the scope). Holidays,/admin/status, and the jira-sync trigger are left unannotated on purpose (not token-facing → unreachable by tokens).OpenAPI (
public/api.yml)Added the
bearerAuth(+cookieAuth)securityScheme, a top-levelsecurity, and the token/scope model in the description. Per-operation scope tags are intentionally not duplicated into the static YAML — the code's#[RequireScope]is the single source of truth, so spec and enforcement can't drift.Coverage
RequireScopeCoverageTest— every declared#[RequireScope]uses a validApiScope(catches typos that would silently break an endpoint) + a floor count so annotations can't vanish.ApiTokenAuthTest— a Phase-4 endpoint is reachable with its scope and denied with a wrong one; the fail-closed example moved to a still-unannotated endpoint.Gates: PHPStan L10, php-cs-fixer, Rector, phpat clean; full suite 2185 tests green (session access unaffected —
RequireScopeSubscriberonly gates token requests).