fix: add is_public filtering to /projects and /boards endpoints #185
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/pal-e-api!185
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "184-projects-boards-is-public-filtering"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Unauthenticated requests to
/projectsand/boardsendpoints now respectis_publicfiltering, matching the pattern established in F6 (PR #179) for notes. Previously, all projects and boards were returned regardless of authentication, exposing private projects like "Private" and "Remember" to public visitors.Changes
src/pal_e_docs/routes/projects.py-- Addedget_is_authenticateddependency tolist_projects(filtersis_public=falseprojects) andget_project(returns 404 for private projects when unauthenticated)src/pal_e_docs/routes/boards.py-- Addedget_is_authenticateddependency tolist_boards(joins to Project and filters byis_public) andget_board(returns 404 when parent project is private and unauthenticated)tests/test_private_projects_boards.py-- 22 new tests covering: backwards compatibility (no API key), unauthenticated filtering, authenticated access, wrong token, edge cases (all private, nonexistent slugs)Test Plan
pytest tests/test_private_projects_boards.py -v-- 22 passedpytest tests/ -v-- 599 passed (full suite, no regressions)ruff formatandruff checkcleanReview Checklist
routes/notes.pyauth filtering)Related
phase-pal-e-docs-f14-public-readinessSelf-review caught three additional data leak vectors: - GET /boards/backlog/items returned items from private project boards - GET /boards/activity returned items from private project boards - GET /boards/{slug}/items did not check parent project is_public All three now join through Board->Project and filter by is_public when unauthenticated. Adds 7 tests covering items, backlog, and activity endpoints for both auth states. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>Self-Review Findings
Issues Found and Fixed (commit
254e5b0)3 additional data leak vectors in board sub-resource endpoints were not covered by the initial commit:
GET /boards/backlog/items-- Cross-board backlog returned items from private project boards to unauthenticated users. Fixed by joiningBoardItem -> Board -> Projectand filteringProject.is_public == True.GET /boards/activity-- Cross-board activity returned items from private project boards. Same join+filter fix.GET /boards/{slug}/items-- Board items endpoint did not check parent projectis_public. Fixed by checkingboard.project.is_publicand returning 404, same pattern asget_board.Tests Added
7 new tests covering the three endpoints (29 total, up from 22):
test_board_items_unauthenticated_private_project_returns_404test_board_items_unauthenticated_public_project_returns_200test_board_items_authenticated_private_project_returns_200test_backlog_items_unauthenticated_excludes_private_projecttest_backlog_items_authenticated_includes_alltest_activity_unauthenticated_excludes_private_projecttest_activity_authenticated_includes_allRemaining Known Gap (documented, not a blocker)
GET /projects/{slug}/notesdoes not checkproject.is_public-- it only filters notes bynote.is_public. This means if you know a private project's slug, you can hit this endpoint and get back public notes from it (empty list if all notes are private). This is acceptable because no private data leaks -- only public notes are returned. Documented in testtest_project_notes_endpoint_respects_project_visibility.Verification
pytest tests/ -v-- 606 passed (0 failures)ruff format+ruff check-- cleanVERDICT: APPROVE -- all acceptance criteria met, additional leak vectors caught and fixed during review.
PR #185 Review
DOMAIN REVIEW
Tech stack: Python / FastAPI / SQLAlchemy (with SQLite test DB). This is a security-relevant change -- adding
is_publicfiltering to read endpoints for projects and boards, extending the pattern established in PR #179 for notes.Pattern consistency: The implementation correctly mirrors the approach from
routes/notes.py. Theget_is_authenticateddependency is injected into all affected read endpoints, and the filtering logic follows the sameif not is_authenticated: q = q.filter(Project.is_public == True)pattern. The# noqa: E712comments are correct -- SQLAlchemy requires== Truerather thanis Truefor column comparisons.Endpoints covered by this PR:
GET /projects-- filters byProject.is_public(list)GET /projects/{slug}-- returns 404 for private when unauthenticated (detail)GET /boards-- joins to Project, filters byProject.is_public(list)GET /boards/{slug}-- returns 404 for private project when unauthenticated (detail)GET /boards/{slug}/items-- returns 404 for private project board when unauthenticatedGET /boards/backlog/items-- joins Board->Project, filters byProject.is_publicGET /boards/activity-- joins Board->Project, filters byProject.is_publicSQLAlchemy join correctness: The joins in
list_backlog_itemsandlist_activitycorrectly chainBoardItem -> Board -> Projectusing explicit join conditions (BoardItem.board_id == Board.id,Board.project_id == Project.id). Thelist_boardsendpoint joinsBoard -> Projectsince Board has a direct FK to Project. All correct.get_board/list_board_itemsapproach: These useboard.project.is_publicvia lazy-loaded relationship rather than a join. This is fine for single-row lookups -- the lazy load is a single query and avoids unnecessary join complexity for a detail endpoint.Auth module (
auth.py): Usessecrets.compare_digestfor timing-safe comparison. Backwards compatibility is preserved: when no API key is configured,get_is_authenticatedreturnsTrueunconditionally. Solid.Test coverage (22 tests):
is_publicfield in response)Coverage is thorough. Happy paths, sad paths, edge cases, and cross-endpoint interactions are all represented.
test_project_notes_endpoint_respects_project_visibility: This test correctly documents thatGET /projects/{slug}/notesdoes NOT check project-levelis_public-- it finds the project by slug and only filters notes byNote.is_public. The test comment explains this is acceptable because only public notes are returned. This is a fair design decision: the endpoint leaks that a project slug exists but does not expose private content. Not a blocker, but worth noting for future hardening.BLOCKERS
None. All blocker criteria pass:
get_is_authenticateddependency.auth.py:get_is_authenticated. Each endpoint only applies the boolean result -- no duplicated auth logic.NITS
Write endpoints lack auth gating (out of scope, but worth a follow-up issue):
create_board,update_board,delete_board,sync_board,sync_issues,bulk_move_items,create_board_item,update_board_item,delete_board_item,create_project,update_project,delete_project-- none of these checkis_authenticated. An unauthenticated request can create/modify/delete projects and boards. This is pre-existing and outside the scope of #184, but should be tracked as a separate issue.list_project_notesproject-level visibility (documented in test): As noted above,GET /projects/{slug}/notesdoes not gate onproject.is_public. The test documents this intentionally. A future hardening pass could return 404 for private project slugs here too, but the current behavior is safe since only public notes are returned.Test helper
_create_projects_and_boardscreates data outsidepatchcontext: In tests liketest_no_api_key_all_projects_visible, the helper is called without patchingpal_e_docs.auth.settings, which meansget_is_authenticatedreturnsTrue(no API key configured). This is correct for the "no API key" tests. For the "with API key" tests, the patch is active during both creation and querying, which is also correct. No issue here -- just noting the pattern is intentional and sound.SOP COMPLIANCE
184-projects-boards-is-public-filtering(references #184)phase-pal-e-docs-f14-public-readinesstests/test_private_projects_boards.pyfix: add is_public filtering to /projects and /boards endpoints)PROCESS OBSERVATIONS
VERDICT: APPROVED