mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-26 06:16:56 +09:00
The token a request authenticated with now lives on the doer, so permission and visibility checks apply its scope from the doer alone. Replaces https://github.com/go-gitea/gitea/pull/39167, following https://github.com/go-gitea/gitea/pull/39167#issuecomment-6063839619. 1. Public-only tokens get 404 instead of 403 for hidden users, orgs and repos, like GitHub. 2. Org ownership, site admin, repo and org creation, generation and transfer endpoints reject public-only tokens. 3. API pull request responses set `head.repo` to `null` and `head.repo_id` to `-1` when the caller cannot see the head repository. 4. Public-only tokens see org memberships like anonymous callers, so concealed members stay hidden. 5. Site admin access in activity feeds, follower lists, org members, Actions run lists and packages needs a session or an `all`-scoped token. Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
181 lines
8.8 KiB
Go
181 lines
8.8 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
|
|
auth_model "gitea.dev/models/auth"
|
|
"gitea.dev/models/db"
|
|
"gitea.dev/models/unittest"
|
|
user_model "gitea.dev/models/user"
|
|
api "gitea.dev/modules/structs"
|
|
"gitea.dev/tests"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestAPIPublicOnlySelfUserRoutes(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user31"})
|
|
require.True(t, privateUser.Visibility.IsPrivate())
|
|
|
|
privateSession := loginUser(t, privateUser.Name)
|
|
privateReadUserToken := getTokenForLoggedInUser(t, privateSession,
|
|
auth_model.AccessTokenScopePublicOnly,
|
|
auth_model.AccessTokenScopeReadUser,
|
|
)
|
|
privateWriteUserToken := getTokenForLoggedInUser(t, privateSession,
|
|
auth_model.AccessTokenScopePublicOnly,
|
|
auth_model.AccessTokenScopeWriteUser,
|
|
)
|
|
|
|
t.Run("PrivateProfileUnreachable", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/user31").AddTokenAuth(privateReadUserToken), http.StatusNotFound)
|
|
require.NoError(t, db.Insert(t.Context(), &user_model.Redirect{LowerName: "old-user31", RedirectUserID: privateUser.ID}))
|
|
MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/old-user31").AddTokenAuth(privateReadUserToken), http.StatusNotFound)
|
|
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
|
})
|
|
|
|
t.Run("PrivateSensitiveSelfRoutesForbidden", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/settings").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
|
hideEmail := true
|
|
settingsReq := NewRequestWithJSON(t, "PATCH", "/api/v1/user/settings", &api.UserSettingsOptions{
|
|
HideEmail: &hideEmail,
|
|
}).AddTokenAuth(privateWriteUserToken)
|
|
MakeRequest(t, settingsReq, http.StatusForbidden)
|
|
|
|
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/emails").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
|
emailReq := NewRequestWithJSON(t, "POST", "/api/v1/user/emails", &api.CreateEmailOption{
|
|
Emails: []string{"user31-public-only@example.com"},
|
|
}).AddTokenAuth(privateWriteUserToken)
|
|
MakeRequest(t, emailReq, http.StatusForbidden)
|
|
|
|
keyReq := NewRequestWithJSON(t, "POST", "/api/v1/user/keys", api.CreateKeyOption{
|
|
Title: "public-only-private-key",
|
|
Key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment",
|
|
}).AddTokenAuth(privateWriteUserToken)
|
|
MakeRequest(t, keyReq, http.StatusForbidden)
|
|
|
|
oauthReq := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &api.CreateOAuth2ApplicationOptions{
|
|
Name: "public-only-private-oauth-app",
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
ConfidentialClient: true,
|
|
}).AddTokenAuth(privateWriteUserToken)
|
|
MakeRequest(t, oauthReq, http.StatusForbidden)
|
|
|
|
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/gpg_keys").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
|
gpgKeyReq := NewRequestWithJSON(t, "POST", "/api/v1/user/gpg_keys", &api.CreateGPGKeyOption{
|
|
ArmoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\ncomment\n-----END PGP PUBLIC KEY BLOCK-----",
|
|
}).AddTokenAuth(privateWriteUserToken)
|
|
MakeRequest(t, gpgKeyReq, http.StatusForbidden)
|
|
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/gpg_key_token").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
|
gpgVerifyReq := NewRequestWithJSON(t, "POST", "/api/v1/user/gpg_key_verify", &api.VerifyGPGKeyOption{
|
|
KeyID: "deadbeef",
|
|
Signature: "invalid-signature",
|
|
}).AddTokenAuth(privateWriteUserToken)
|
|
MakeRequest(t, gpgVerifyReq, http.StatusForbidden)
|
|
|
|
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/actions/variables").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
|
MakeRequest(t, NewRequest(t, "DELETE", "/api/v1/user/actions/secrets/PRIVATE_SECRET").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
|
|
variableReq := NewRequestWithJSON(t, "POST", "/api/v1/user/actions/variables/PRIVATE_VAR", api.CreateVariableOption{
|
|
Value: "private-value",
|
|
Description: "must stay private",
|
|
}).AddTokenAuth(privateWriteUserToken)
|
|
MakeRequest(t, variableReq, http.StatusForbidden)
|
|
|
|
MakeRequest(t, NewRequest(t, "POST", "/api/v1/user/actions/runners/registration-token").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
|
|
|
|
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/hooks").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
|
hookReq := NewRequestWithJSON(t, "POST", "/api/v1/user/hooks", api.CreateHookOption{
|
|
Type: "gitea",
|
|
Config: api.CreateHookOptionConfig{
|
|
"content_type": "json",
|
|
"url": "http://example.com/",
|
|
},
|
|
Name: "public-only-private-hook",
|
|
}).AddTokenAuth(privateWriteUserToken)
|
|
MakeRequest(t, hookReq, http.StatusForbidden)
|
|
|
|
avatarReq := NewRequestWithJSON(t, "POST", "/api/v1/user/avatar", &api.UpdateUserAvatarOption{
|
|
Image: "aGVsbG8=",
|
|
}).AddTokenAuth(privateWriteUserToken)
|
|
MakeRequest(t, avatarReq, http.StatusForbidden)
|
|
MakeRequest(t, NewRequest(t, "DELETE", "/api/v1/user/avatar").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
|
|
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/times").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
|
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/stopwatches").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
|
|
|
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/subscriptions").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
|
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/teams").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
|
|
|
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/blocks").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
|
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/user/blocks/user2").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
|
|
|
|
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/user/following/user2").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
|
|
MakeRequest(t, NewRequest(t, "DELETE", "/api/v1/user/following/user2").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
|
|
})
|
|
|
|
t.Run("PublicRepoRoutesFilterAndRejectMutations", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
publicSession := loginUser(t, "user2")
|
|
fullWriteRepoToken := getTokenForLoggedInUser(t, publicSession,
|
|
auth_model.AccessTokenScopeWriteUser,
|
|
auth_model.AccessTokenScopeWriteRepository,
|
|
)
|
|
publicOnlyReadRepoToken := getTokenForLoggedInUser(t, publicSession,
|
|
auth_model.AccessTokenScopePublicOnly,
|
|
auth_model.AccessTokenScopeReadUser,
|
|
auth_model.AccessTokenScopeReadRepository,
|
|
)
|
|
publicOnlyWriteRepoToken := getTokenForLoggedInUser(t, publicSession,
|
|
auth_model.AccessTokenScopePublicOnly,
|
|
auth_model.AccessTokenScopeWriteUser,
|
|
auth_model.AccessTokenScopeWriteRepository,
|
|
)
|
|
|
|
publicRepoName := "public-only-visible-self-repo"
|
|
privateRepoName := "public-only-hidden-self-repo"
|
|
|
|
resp := MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
|
|
Name: publicRepoName,
|
|
Private: false,
|
|
}).AddTokenAuth(fullWriteRepoToken), http.StatusCreated)
|
|
publicRepo := DecodeJSON(t, resp, &api.Repository{})
|
|
require.Equal(t, "user2/"+publicRepoName, publicRepo.FullName)
|
|
|
|
resp = MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
|
|
Name: privateRepoName,
|
|
Private: true,
|
|
}).AddTokenAuth(fullWriteRepoToken), http.StatusCreated)
|
|
privateRepo := DecodeJSON(t, resp, &api.Repository{})
|
|
require.Equal(t, "user2/"+privateRepoName, privateRepo.FullName)
|
|
|
|
MakeRequest(t, NewRequest(t, "GET", "/api/v1/repos/user2/"+privateRepoName).AddTokenAuth(publicOnlyReadRepoToken), http.StatusNotFound)
|
|
|
|
resp = MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/repos").AddTokenAuth(publicOnlyReadRepoToken), http.StatusOK)
|
|
repos := DecodeJSON(t, resp, []api.Repository{})
|
|
|
|
foundPublicRepo := false
|
|
for _, repo := range repos {
|
|
require.NotEqual(t, privateRepo.FullName, repo.FullName)
|
|
if repo.FullName == publicRepo.FullName {
|
|
foundPublicRepo = true
|
|
}
|
|
}
|
|
require.True(t, foundPublicRepo)
|
|
|
|
MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
|
|
Name: "public-only-rejected-self-repo",
|
|
Private: false,
|
|
}).AddTokenAuth(publicOnlyWriteRepoToken), http.StatusForbidden)
|
|
})
|
|
}
|