fix: hide user profile anonymous options on public repo APIs

This commit is contained in:
Mathieu Fenniak 2025-12-18 09:47:40 -07:00
parent 8e083c9f3e
commit bade14ee69
No known key found for this signature in database
3 changed files with 50 additions and 5 deletions

View file

@ -46,6 +46,7 @@
email: user2@example.com
keep_email_private: true
keep_pronouns_private: true
pronouns: he/him
email_notifications_preference: enabled
passwd: ZogKvWdyEx:password
passwd_hash_algo: dummy

View file

@ -4,7 +4,7 @@
package convert
import (
"context"
stdCtx "context"
"time"
"forgejo.org/models"
@ -15,14 +15,15 @@ import (
unit_model "forgejo.org/models/unit"
"forgejo.org/modules/log"
api "forgejo.org/modules/structs"
"forgejo.org/services/context"
)
// ToRepo converts a Repository to api.Repository
func ToRepo(ctx context.Context, repo *repo_model.Repository, permissionInRepo access_model.Permission) *api.Repository {
func ToRepo(ctx stdCtx.Context, repo *repo_model.Repository, permissionInRepo access_model.Permission) *api.Repository {
return innerToRepo(ctx, repo, permissionInRepo, false)
}
func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInRepo access_model.Permission, isParent bool) *api.Repository {
func innerToRepo(ctx stdCtx.Context, repo *repo_model.Repository, permissionInRepo access_model.Permission, isParent bool) *api.Repository {
var parent *api.Repository
if permissionInRepo.Units == nil && permissionInRepo.UnitsMode == nil {
@ -179,9 +180,19 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
repoAPIURL := repo.APIURL()
// Calculate the effective permission for `ToUserWithAccessMode` for the repo owner. When accessing a public repo,
// permissionInRepo.AccessMode will be AccessModeRead even for an anonymous user -- in that case, downgrade
// `ownerViewPerms` to `AccessModeNone`. `innerToRepo` doesn't have great access to recognize an anonymous user, so
// the best-effort made here is to check if `ctx` is an `APIContext`.
ownerViewPerms := permissionInRepo.AccessMode
apiCtx, ok := ctx.(*context.APIContext)
if ok && apiCtx.Doer == nil {
ownerViewPerms = perm.AccessModeNone
}
return &api.Repository{
ID: repo.ID,
Owner: ToUserWithAccessMode(ctx, repo.Owner, permissionInRepo.AccessMode),
Owner: ToUserWithAccessMode(ctx, repo.Owner, ownerViewPerms),
Name: repo.Name,
FullName: repo.FullName(),
Description: repo.Description,
@ -246,7 +257,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
}
// ToRepoTransfer convert a models.RepoTransfer to a structs.RepeTransfer
func ToRepoTransfer(ctx context.Context, t *models.RepoTransfer) *api.RepoTransfer {
func ToRepoTransfer(ctx stdCtx.Context, t *models.RepoTransfer) *api.RepoTransfer {
teams, _ := ToTeams(ctx, t.Teams, false)
return &api.RepoTransfer{

View file

@ -289,6 +289,39 @@ func TestAPIViewRepo(t *testing.T) {
assert.Equal(t, 1, repo.Stars)
}
// Validate that private information on the user profile isn't exposed by way of being an owner of a public repository.
func TestAPIViewRepoOwnerSettings(t *testing.T) {
defer tests.PrepareTestEnv(t)()
var repo api.Repository
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1")
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repo)
assert.EqualValues(t, 1, repo.ID)
assert.Equal(t, "user2@noreply.example.org", repo.Owner.Email) // unauthed, always private
assert.Empty(t, repo.Owner.Pronouns) // user2.keep_pronouns_private = true
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1").AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repo)
assert.Equal(t, "user2@noreply.example.org", repo.Owner.Email) // user2.keep_email_private = true
assert.Equal(t, "he/him", repo.Owner.Pronouns) // user2.keep_pronouns_private = true
req = NewRequest(t, "GET", "/api/v1/repos/user12/repo10")
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repo)
assert.EqualValues(t, 10, repo.ID)
assert.Equal(t, "user12@noreply.example.org", repo.Owner.Email) // unauthed, always private
req = NewRequest(t, "GET", "/api/v1/repos/user12/repo10").AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repo)
assert.Equal(t, "user12@example.com", repo.Owner.Email) // user2.keep_email_private = false
}
func TestAPIOrgRepos(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})