Skip to content

fix: Unify public-only token filtering in API queries and repo access checks#37118

Merged
lunny merged 17 commits into
go-gitea:mainfrom
lunny:lunny/fix_public_only_list_org
May 18, 2026
Merged

fix: Unify public-only token filtering in API queries and repo access checks#37118
lunny merged 17 commits into
go-gitea:mainfrom
lunny:lunny/fix_public_only_list_org

Conversation

@lunny
Copy link
Copy Markdown
Member

@lunny lunny commented Apr 6, 2026

This PR closes remaining public-only token gaps in the API by making the restriction apply consistently across repository, organization, activity, notification, and authenticated /api/v1/user/... routes.

Previously, public-only tokens were still able to:

  • receive private results from some list/search/self endpoints,
  • access repository data through ID-based lookups,
  • and reach several authenticated self routes that should remain unavailable for public-only access.

This change treats public-only as a cross-cutting visibility boundary:

  • list/search endpoints now filter private resources consistently,
  • repository lookups enforce the same restriction even when addressed indirectly,
  • and self routes that inherently expose or mutate private account state now reject public-only tokens.

What changed

Shared public-only filtering helpers

Added ApplyPublicOnly helpers to option structs used by search/list APIs so callers can enforce public-only filtering uniformly:

  • models/user/search.go
  • models/organization/org_list.go
  • models/repo/repo_list.go
  • models/repo/user_repo.go
  • models/activities/action.go

These helpers are now used by:

  • user search,
  • organization listing,
  • repo search and user repo listing,
  • starred/watched repo listing,
  • and activity feed queries.

Repository access hardening

Added TokenCanAccessRepo() in services/context/api.go and used it to reject private repository access for public-only tokens even when the repo is resolved indirectly.

This now protects cases such as:

  • /api/v1/repositories/{id}
  • repo assignment based lookups
  • repository-scoped public-only checks that previously only relied on route-level context

/api/v1/user self-route hardening

The authenticated /api/v1/user route group now sets ctx.ContextUser = ctx.Doer before public-only checks and applies checkTokenPublicOnly() consistently.

For endpoints that are inherently private account surfaces, this PR explicitly rejects public-only tokens via rejectPublicOnly().

This includes representative self routes such as:

  • /api/v1/user
  • /api/v1/user/settings
  • /api/v1/user/emails
  • /api/v1/user/keys
  • /api/v1/user/gpg_keys
  • /api/v1/user/gpg_key_token
  • /api/v1/user/gpg_key_verify
  • /api/v1/user/applications/oauth2
  • /api/v1/user/actions/...
  • /api/v1/user/hooks
  • /api/v1/user/avatar
  • /api/v1/user/times
  • /api/v1/user/stopwatches
  • /api/v1/user/teams
  • /api/v1/user/blocks
  • authenticated self follow/unfollow mutations
  • private self repo creation

For self list endpoints that can safely expose only public data, the behavior remains filtered rather than fully blocked where appropriate.

Other route fixes

Also tightened public-only handling for:

  • /api/v1/notifications and related thread endpoints
  • /api/v1/user/orgs
  • user starred repo endpoints
  • watched repo endpoints
  • user/org/repo activity feeds

Why

public-only tokens are intended to be limited to public resources.

This PR fixes cases where that boundary was incomplete:

  • some endpoints returned private resources instead of filtering them out,
  • some authenticated self routes skipped public-only enforcement entirely,
  • and some route families behaved differently from their canonical public/private counterparts.

The result is a more consistent rule set:

  • public-only tokens can access public resources,
  • they cannot access private repositories or private user/org data through alternate route shapes,
  • and they cannot mutate inherently private self-account resources.

Testing

Added and updated integration coverage for:

  • public-only filtering on:

    • user repos
    • repo-by-ID access
    • activity feeds
    • watched/starred repos
    • user orgs
  • public-only rejection on:

    • notifications
    • authenticated self /api/v1/user/... private surfaces

Representative self-route regression coverage now includes:

  • profile/settings
  • emails
  • SSH keys
  • GPG keys and verification token endpoints
  • OAuth applications
  • Actions secrets/variables/runners
  • hooks
  • avatar
  • times/stopwatches
  • subscriptions/teams
  • blocks
  • follow/unfollow
  • self repo creation/list filtering

Generated by a coding agent with Codex 5.2

@GiteaBot GiteaBot added the lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. label Apr 6, 2026
@lunny lunny marked this pull request as ready for review April 6, 2026 19:39
@silverwind
Copy link
Copy Markdown
Member

silverwind commented Apr 7, 2026

A few points worth addressing:

  • The scattered if ctx.PublicOnly { private = false } checks across 6+ handlers are fragile and easy to miss when adding new endpoints. A cleaner approach would be to push PublicOnly into the query layer: add a PublicOnly bool field to the search/find option structs (SearchRepoOptions, FindOrgOptions, StarredReposOptions, WatchedReposOptions, etc.), set it once from ctx.PublicOnly, and let the database queries handle the exclusion. For entity-level checks (like GetByID), a centralized tokenCanAccessRepo(ctx, repo) called from repoAssignment() would also reduce the surface area for missed checks.

  • rejectPublicOnly() hardcodes "token scope is limited to public notifications" — should use a generic message like "this endpoint is not available for public-only tokens" or accept a parameter.

  • The explanatory comment about notifications is placed after the middleware registration — should go before it.

  • TestAPIActivityFeedsPublicOnly asserts Empty(t, activities), which only works because all test fixture activity happens to be on private repos. If someone adds public activity for user2/org3, the test would still pass but no longer actually test the filtering. Better to assert that no returned activity references a private repo.


This comment was written with the help of Claude.

@lunny lunny force-pushed the lunny/fix_public_only_list_org branch from e6a1872 to 4c8cc91 Compare May 15, 2026 03:48
@lunny lunny changed the title Fix public-only token enforcement for user/org APIs and notifications fix: public-only token enforcement for user/org APIs and notifications May 15, 2026
@lunny lunny changed the title fix: public-only token enforcement for user/org APIs and notifications fix: Unify public-only token filtering in API queries and repo access checks May 15, 2026
@lunny lunny added type/bug backport/v1.26 This PR should be backported to Gitea 1.26 labels May 15, 2026
@lunny
Copy link
Copy Markdown
Member Author

lunny commented May 15, 2026

  • rejectPublicOnly

Resolved.

@bircni bircni mentioned this pull request May 17, 2026
25 tasks
@GiteaBot GiteaBot added lgtm/need 1 This PR needs approval from one additional maintainer to be merged. and removed lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. labels May 17, 2026
Comment thread tests/integration/api_user_star_test.go Outdated
resp := MakeRequest(t, req, http.StatusOK)

var repos []api.Repository
DecodeJSON(t, resp, &repos)
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.

chore: clean up tests #37715

  1. use modern generic syntax for remaining "DecodeJSON" calls

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Comment thread tests/integration/api_user_star_test.go Outdated
Comment on lines +175 to +177
if assert.Len(t, repos, 1) {
assert.Equal(t, "user5/repo4", repos[0].FullName)
}
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.

require.Len(t, repos, 1)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@silverwind
Copy link
Copy Markdown
Member

silverwind commented May 18, 2026

Pushing some fixups. Status code will be 404 to hide existance of private repos.

Return 404 instead of 403 in repoAssignment and GetByID when a
public-only token references a private repo, matching GitHub's
convention of hiding repository existence.

Also tidy the public-only checks:
- TokenCanAccessRepo becomes a method on APIContext
- Use VisibleType.IsPublic() instead of inline comparisons
- Fold the two Organization sub-checks into one branch
- Add coverage for watched-repo filtering

Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
Two integration tests still expected 403 from the API path where
repoAssignment now returns 404 for public-only tokens accessing
private repos. Align the assertions with the new behavior.

Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
@GiteaBot GiteaBot added lgtm/done This PR has enough approvals to get merged. There are no important open reservations anymore. and removed lgtm/need 1 This PR needs approval from one additional maintainer to be merged. labels May 18, 2026
@lunny lunny merged commit f2a1271 into go-gitea:main May 18, 2026
22 checks passed
@GiteaBot GiteaBot added this to the 1.27.0 milestone May 18, 2026
@lunny lunny deleted the lunny/fix_public_only_list_org branch May 18, 2026 18:37
@GiteaBot
Copy link
Copy Markdown
Collaborator

I was unable to create a backport for 1.26. @lunny, please send one manually. 🍵

go run ./contrib/backport 37118
...  // fix git conflicts if any
go run ./contrib/backport --continue

@GiteaBot GiteaBot added the backport/manual No power to the bots! Create your backport yourself! label May 18, 2026
@lunny lunny added the backport/done All backports for this PR have been created label May 19, 2026
bircni added a commit that referenced this pull request May 19, 2026
… checks (#37118) (#37773)

backport #37118 

This PR closes remaining `public-only` token gaps in the API by making
the restriction apply consistently across repository, organization,
activity, notification, and authenticated `/api/v1/user/...` routes.

Previously, `public-only` tokens were still able to:
- receive private results from some list/search/self endpoints,
- access repository data through ID-based lookups,
- and reach several authenticated self routes that should remain
unavailable for public-only access.

This change treats `public-only` as a cross-cutting visibility boundary:
- list/search endpoints now filter private resources consistently,
- repository lookups enforce the same restriction even when addressed
indirectly,
- and self routes that inherently expose or mutate private account state
now reject `public-only` tokens.

---
Generated by a coding agent with Codex 5.2

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: Nicolas <bircni@icloud.com>
silverwind added a commit to silverwind/gitea that referenced this pull request May 19, 2026
* origin/main: (104 commits)
  fix(deps): update module github.com/go-git/go-git/v5 to v5.19.1 [security] (go-gitea#37786)
  fix(pull): handle empty pull request files view to allow reviews (go-gitea#37783)
  fix(markup): make RenderString never fail (go-gitea#37779)
  fix(markup): wrap indented code blocks for the code-copy button (go-gitea#37748)
  fix(permissions): Fix reading permission (go-gitea#37769)
  fix: add natural sort to sortTreeViewNodes (go-gitea#37772)
  fix: package creation unique conflict (go-gitea#37774)
  fix(deps): update npm dependencies (go-gitea#37768)
  fix(deps): update module gitlab.com/gitlab-org/api/client-go/v2 to v2.26.0 (go-gitea#37771)
  ci: split giteabot workflow (go-gitea#37770)
  [skip ci] Updated translations via Crowdin
  fix: Unify public-only token filtering in API queries and repo access checks (go-gitea#37118)
  fix(deps): update module google.golang.org/grpc to v1.81.1 (go-gitea#37762)
  chore: make DefaultTitleSource default to auto to match GitHub (go-gitea#37767)
  ci: fix cache-related issues (go-gitea#37761)
  chore: fix tests (go-gitea#37760)
  refactor(waitgroup): replace Add/Done goroutines with WaitGroup.Go (go-gitea#37764)
  fix(deps): update go dependencies (go-gitea#37752)
  chore(deps): update action dependencies (go-gitea#37751)
  fix(deps): update module github.com/google/go-github/v85 to v86 (go-gitea#37754)
  ...

# Conflicts:
#	.github/workflows/pull-db-tests.yml
#	modules/storage/s3_test.go
eleboucher pushed a commit to eleboucher/apoci that referenced this pull request May 20, 2026
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [code.gitea.io/gitea](https://github.com/go-gitea/gitea) | `v1.26.1` → `v1.26.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/code.gitea.io%2fgitea/v1.26.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/code.gitea.io%2fgitea/v1.26.1/v1.26.2?slim=true) |

---

### Release Notes

<details>
<summary>go-gitea/gitea (code.gitea.io/gitea)</summary>

### [`v1.26.2`](https://github.com/go-gitea/gitea/releases/tag/v1.26.2)

[Compare Source](go-gitea/gitea@v1.26.1...v1.26.2)

- SECURITY
  - fix(permissions): Fix reading permission ([#&#8203;37769](go-gitea/gitea#37769))
  - fix(actions): make artifact signature payloads unambiguous ([#&#8203;37707](go-gitea/gitea#37707))
  - fix: Unify public-only token filtering in API queries and repo access checks ([#&#8203;37118](go-gitea/gitea#37118))
  - fix: Add missed token scope checking ([#&#8203;37735](go-gitea/gitea#37735))
  - fix(oauth): bind token exchanges to the original client request ([#&#8203;37704](go-gitea/gitea#37704))
  - fix(oauth): strengthen PKCE validation and refresh token replay protection ([#&#8203;37706](go-gitea/gitea#37706))
  - fix(web): enforce token scopes on raw, media, and attachment downloads ([#&#8203;37698](go-gitea/gitea#37698))
  - fix(security): enforce wiki git writes and LFS token access at request time ([#&#8203;37695](go-gitea/gitea#37695))
  - feat(api): encrypt AWS creds ([#&#8203;37679](go-gitea/gitea#37679))
  - fix(deps): update dependency mermaid to v11.15.0 \[security], add e2e test
  - fix(packages): Add label for private and internal package and fix composor package source permission check ([#&#8203;37610](go-gitea/gitea#37610))
  - fix(git): Fix smart http request scope bug ([#&#8203;37583](go-gitea/gitea#37583))
  - Fix basic auth bug ([#&#8203;37503](go-gitea/gitea#37503))
  - Fix allow maintainer edit permission check ([#&#8203;37479](go-gitea/gitea#37479)) ([#&#8203;37484](go-gitea/gitea#37484))
  - Fix URL sanitization to handle schemeless credentials ([#&#8203;37440](go-gitea/gitea#37440)) ([#&#8203;37471](go-gitea/gitea#37471))
  - Fix attachment Content-Security-Policy ([#&#8203;37455](go-gitea/gitea#37455)) ([#&#8203;37464](go-gitea/gitea#37464))
  - chore(deps): bump go-git/go-git/v5 to 5.19.0 ([#&#8203;37608](go-gitea/gitea#37608))

- BUGFIXES
  - fix(pull): handle empty pull request files view to allow reviews ([#&#8203;37783](go-gitea/gitea#37783))
  - fix(markup): make RenderString never fail ([#&#8203;37779](go-gitea/gitea#37779))
  - fix: add natural sort to sortTreeViewNodes ([#&#8203;37772](go-gitea/gitea#37772))
  - fix: package creation unique conflict ([#&#8203;37774](go-gitea/gitea#37774))
  - fix!: add DEFAULT\_TITLE\_SOURCE setting for pull request title default behavior ([#&#8203;37465](go-gitea/gitea#37465))
  - fix: Allow direct commits for unprotected files with push restrictions ([#&#8203;37657](go-gitea/gitea#37657))
  - fix(actions): wrong assumption that run id always >= job id ([#&#8203;37737](go-gitea/gitea#37737))
  - fix(auth): set User-Agent on avatar fetch and sync avatar on link-account register ([#&#8203;37564](go-gitea/gitea#37564)) ([#&#8203;37588](go-gitea/gitea#37588))
  - fix(actions): deadlock between PrepareRunAndInsert and UpdateTaskByState ([#&#8203;37692](go-gitea/gitea#37692))
  - fix(repo): /generate must sync the branch table for the new repo ([#&#8203;37693](go-gitea/gitea#37693))
  - build: Fix snap build (1.26)
  - fix(actions): run TransferLogs on UpdateLog{Rows:\[], NoMore:true} ([#&#8203;37631](go-gitea/gitea#37631))
  - fix show correct mergebase
  - fix: make clone URL respect public URL detection setting ([#&#8203;37615](go-gitea/gitea#37615))
  - fix: "run as root" check ([#&#8203;37622](go-gitea/gitea#37622))
  - chore(deps): update dependency go to v1.26.3 ([#&#8203;37601](go-gitea/gitea#37601))
  - Compare dropdown fails when selecting branch with no common merge-base ([#&#8203;37470](go-gitea/gitea#37470))
  - fix: treat email addresses case-insensitively ([#&#8203;37600](go-gitea/gitea#37600))
  - fix(actions): fix blank lines after ::endgroup:: ([#&#8203;37597](go-gitea/gitea#37597))
  - fix(actions): report individual step status in workflow job API response ([#&#8203;37592](go-gitea/gitea#37592))
  - fix: Invalid UTF-8 commit messages in JSON API responses ([#&#8203;37542](go-gitea/gitea#37542))
  - fix: use consistent GetUser family functions ([#&#8203;37553](go-gitea/gitea#37553))
  - fix(api): return 409 message instead of empty JSON for wrong commit id ([#&#8203;37572](go-gitea/gitea#37572))
  - fix(actions): prevent panic when workflow contains null jobs ([#&#8203;37570](go-gitea/gitea#37570))
  - Make ServeSetHeaders default to download attachment if filename exists ([#&#8203;37552](go-gitea/gitea#37552)) ([#&#8203;37555](go-gitea/gitea#37555))
  - Fix(actions): validate workflow param to prevent 500 error ([#&#8203;37546](go-gitea/gitea#37546)) ([#&#8203;37554](go-gitea/gitea#37554))
  - Don't unblock run-level-concurrency-blocked runs in the resolver ([#&#8203;37461](go-gitea/gitea#37461)) ([#&#8203;37538](go-gitea/gitea#37538))
  - Fix(packages): use file names for generic web downloads ([#&#8203;37514](go-gitea/gitea#37514)) ([#&#8203;37520](go-gitea/gitea#37520))
  - Fix merge autodetect can't close other PRs but only the last one when multiple PRs are pushed at once ([#&#8203;37512](go-gitea/gitea#37512)) ([#&#8203;37516](go-gitea/gitea#37516))
  - Fix update branch protection order ([#&#8203;37508](go-gitea/gitea#37508)) ([#&#8203;37513](go-gitea/gitea#37513))
  - Fix mCaptcha broken after Vite migration ([#&#8203;37492](go-gitea/gitea#37492)) ([#&#8203;37509](go-gitea/gitea#37509))
  - Fix review submission from single-commit PR view ([#&#8203;37475](go-gitea/gitea#37475)) ([#&#8203;37485](go-gitea/gitea#37485))
  - Fix scheduled action panic with null event payload ([#&#8203;37459](go-gitea/gitea#37459)) ([#&#8203;37466](go-gitea/gitea#37466))
  - Make GetPossibleUserByID can handle deleted user ([#&#8203;37430](go-gitea/gitea#37430)) ([#&#8203;37431](go-gitea/gitea#37431))
  - Remove excessive quote from terraform instructions ([#&#8203;37424](go-gitea/gitea#37424)) ([#&#8203;37426](go-gitea/gitea#37426))
  - Fix color regressions, add `priority` color ([#&#8203;37417](go-gitea/gitea#37417)) ([#&#8203;37421](go-gitea/gitea#37421))

- MISC
  - Add CurrentURL template variable back ([#&#8203;37444](go-gitea/gitea#37444)) ([#&#8203;37449](go-gitea/gitea#37449))

Instances on **[Gitea Cloud](https://cloud.gitea.com)** will be automatically upgraded to this version during the specified maintenance window.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJ0eXBlL3BhdGNoIl19-->

Reviewed-on: https://git.erwanleboucher.dev/eleboucher/apoci/pulls/47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport/done All backports for this PR have been created backport/manual No power to the bots! Create your backport yourself! backport/v1.26 This PR should be backported to Gitea 1.26 lgtm/done This PR has enough approvals to get merged. There are no important open reservations anymore. type/bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants