diff --git a/cmd/hook.go b/cmd/hook.go index 1630c41edd..971c988b5c 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -231,7 +231,7 @@ Forgejo or set your environment appropriately.`, "") continue } - fields := bytes.Fields(scanner.Bytes()) + fields := bytes.Split(scanner.Bytes(), []byte(" ")) if len(fields) != 3 { continue } @@ -397,7 +397,7 @@ Forgejo or set your environment appropriately.`, "") continue } - fields := bytes.Fields(scanner.Bytes()) + fields := bytes.Split(scanner.Bytes(), []byte(" ")) if len(fields) != 3 { continue } diff --git a/cmd/hook_test.go b/cmd/hook_test.go index 89bd3cf737..067c978bdb 100644 --- a/cmd/hook_test.go +++ b/cmd/hook_test.go @@ -14,6 +14,9 @@ import ( "testing" "time" + "forgejo.org/modules/git" + "forgejo.org/modules/json" + "forgejo.org/modules/private" "forgejo.org/modules/setting" "forgejo.org/modules/test" @@ -162,6 +165,136 @@ func TestDelayWriter(t *testing.T) { }) } +func TestRunHookPrePostReceive(t *testing.T) { + // Setup the environment. + defer test.MockVariableValue(&setting.InternalToken, "Random")() + defer test.MockVariableValue(&setting.InstallLock, true)() + defer test.MockVariableValue(&setting.Git.VerbosePush, true)() + t.Setenv("SSH_ORIGINAL_COMMAND", "true") + + tests := []struct { + name string + inputLine string + oldCommitID string + newCommitID string + refFullName string + }{ + { + name: "base case", + inputLine: "00000000000000000000 00000000000000000001 refs/head/main\n", + oldCommitID: "00000000000000000000", + newCommitID: "00000000000000000001", + refFullName: "refs/head/main", + }, + { + name: "nbsp case", + inputLine: "00000000000000000000 00000000000000000001 refs/head/ma\u00A0in\n", + oldCommitID: "00000000000000000000", + newCommitID: "00000000000000000001", + refFullName: "refs/head/ma\u00A0in", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup the Stdin. + f, err := os.OpenFile(t.TempDir()+"/stdin", os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o666) + require.NoError(t, err) + _, err = f.Write([]byte(tt.inputLine)) + require.NoError(t, err) + _, err = f.Seek(0, 0) + require.NoError(t, err) + defer test.MockVariableValue(os.Stdin, *f)() + + // Setup the server that processes the hooks. + var serverError error + var hookOpts *private.HookOptions + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + serverError = err + w.WriteHeader(500) + return + } + + err = json.Unmarshal(body, &hookOpts) + if err != nil { + serverError = err + w.WriteHeader(500) + return + } + + w.WriteHeader(200) + + resp := &private.HookPostReceiveResult{} + bytes, err := json.Marshal(resp) + if err != nil { + serverError = err + return + } + + _, err = w.Write(bytes) + if err != nil { + serverError = err + return + } + })) + defer ts.Close() + defer test.MockVariableValue(&setting.LocalURL, ts.URL+"/")() + + t.Run("pre-receive", func(t *testing.T) { + app := cli.NewApp() + app.Commands = []*cli.Command{subcmdHookPreReceive} + + finish := captureOutput(t, os.Stdout) + err = app.Run([]string{"./forgejo", "pre-receive"}) + require.NoError(t, err) + out := finish() + require.Empty(t, out) + + require.NoError(t, serverError) + require.NotNil(t, hookOpts) + + require.Len(t, hookOpts.OldCommitIDs, 1) + assert.Equal(t, tt.oldCommitID, hookOpts.OldCommitIDs[0]) + require.Len(t, hookOpts.NewCommitIDs, 1) + assert.Equal(t, tt.newCommitID, hookOpts.NewCommitIDs[0]) + require.Len(t, hookOpts.RefFullNames, 1) + assert.Equal(t, git.RefName(tt.refFullName), hookOpts.RefFullNames[0]) + }) + + // seek stdin back to beginning + _, err = f.Seek(0, 0) + require.NoError(t, err) + // reset state from prev test + serverError = nil + hookOpts = nil + + t.Run("post-receive", func(t *testing.T) { + app := cli.NewApp() + app.Commands = []*cli.Command{subcmdHookPostReceive} + + finish := captureOutput(t, os.Stdout) + err = app.Run([]string{"./forgejo", "post-receive"}) + require.NoError(t, err) + out := finish() + require.Empty(t, out) + + require.NoError(t, serverError) + require.NotNil(t, hookOpts) + + require.Len(t, hookOpts.OldCommitIDs, 1) + assert.Equal(t, tt.oldCommitID, hookOpts.OldCommitIDs[0]) + require.Len(t, hookOpts.NewCommitIDs, 1) + assert.Equal(t, tt.newCommitID, hookOpts.NewCommitIDs[0]) + require.Len(t, hookOpts.RefFullNames, 1) + assert.Equal(t, git.RefName(tt.refFullName), hookOpts.RefFullNames[0]) + }) + }) + } +} + func TestRunHookUpdate(t *testing.T) { app := cli.NewApp() app.Commands = []*cli.Command{subcmdHookUpdate} diff --git a/models/asymkey/gpg_key.go b/models/asymkey/gpg_key.go index b7e10ce85c..ba3aae395c 100644 --- a/models/asymkey/gpg_key.go +++ b/models/asymkey/gpg_key.go @@ -110,7 +110,13 @@ func GPGKeyToEntity(ctx context.Context, k *GPGKey) (*openpgp.Entity, error) { if err != nil { return nil, err } - return keys[0], err + + for _, key := range keys { + if key.PrimaryKey.KeyIdString() == k.KeyID { + return key, nil + } + } + return nil, fmt.Errorf("key with %s id not found", k.KeyID) } // parseSubGPGKey parse a sub Key diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 630505b8b4..64a1c85da6 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -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 diff --git a/modules/test/utils.go b/modules/test/utils.go index f60bad022e..2595cf0894 100644 --- a/modules/test/utils.go +++ b/modules/test/utils.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "strings" + "time" "forgejo.org/modules/json" ) @@ -46,3 +47,9 @@ func MockProtect[T any](p *T) (reset func()) { old := *p return func() { *p = old } } + +// When this is called, sleep until the truncated unix time to a minute was +// increased by one. +func SleepTillNextMinute() { + time.Sleep(time.Minute - time.Since(time.Now().Truncate(time.Minute))) +} diff --git a/release-notes/10722.md b/release-notes/10722.md new file mode 100644 index 0000000000..807e660339 --- /dev/null +++ b/release-notes/10722.md @@ -0,0 +1,5 @@ +fix: hide user profile anonymous options on public repo APIs +fix: incorrect whitespace handling on pre&post receive hooks +fix: reduce memory usage while processing large attachment uploads +fix: load reviewer for pull review dismiss action notifier +fix: use correct GPG key for export diff --git a/services/context/api.go b/services/context/api.go index 37f0e0f559..ceec69a3df 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -290,7 +290,7 @@ func APIContexter() func(http.Handler) http.Handler { // If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid. if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") { - if err := ctx.Req.ParseMultipartForm(setting.Attachment.MaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size + if err := ctx.Req.ParseMultipartForm(32 << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size ctx.InternalServerError(err) return } diff --git a/services/context/context.go b/services/context/context.go index 91484c5ba3..8ecfcd04c3 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -181,7 +181,7 @@ func Contexter() func(next http.Handler) http.Handler { // If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid. if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") { - if err := ctx.Req.ParseMultipartForm(setting.Attachment.MaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size + if err := ctx.Req.ParseMultipartForm(32 << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size ctx.ServerError("ParseMultipartForm", err) return } diff --git a/services/convert/repository.go b/services/convert/repository.go index 1b0f46b3da..a075d09455 100644 --- a/services/convert/repository.go +++ b/services/convert/repository.go @@ -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{ diff --git a/services/feed/action.go b/services/feed/action.go index a2cd0551a3..483ecbbde5 100644 --- a/services/feed/action.go +++ b/services/feed/action.go @@ -300,6 +300,10 @@ func (*actionNotifier) AutoMergePullRequest(ctx context.Context, doer *user_mode } func (*actionNotifier) NotifyPullRevieweDismiss(ctx context.Context, doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) { + if err := review.LoadReviewer(ctx); err != nil { + log.Error("LoadReviewer '%d/%d': %v", review.ID, review.ReviewerID, err) + return + } reviewerName := review.Reviewer.Name if len(review.OriginalAuthor) > 0 { reviewerName = review.OriginalAuthor diff --git a/tests/integration/api_gpg_keys_test.go b/tests/integration/api_gpg_keys_test.go index 1acbf91de0..5f2b16b048 100644 --- a/tests/integration/api_gpg_keys_test.go +++ b/tests/integration/api_gpg_keys_test.go @@ -1,19 +1,28 @@ // Copyright 2017 The Gogs Authors. All rights reserved. +// Copyright 2025 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package integration import ( + "bytes" "net/http" "net/http/httptest" "strconv" + "strings" "testing" + "time" auth_model "forgejo.org/models/auth" api "forgejo.org/modules/structs" + "forgejo.org/modules/test" + "forgejo.org/services/context" "forgejo.org/tests" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type makeRequestFunc func(testing.TB, *RequestWrapper, int) *httptest.ResponseRecorder @@ -277,3 +286,222 @@ mIMMn8taHIaQO7v9ln2EVQYTzbNCmwTw9ovTM0j/Pbkg2EftfP1TCoxQHvBnsCED =MgDv -----END PGP PUBLIC KEY BLOCK-----`) } + +func TestAPIGPGMultipleKeys(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + // Token used for verification is valid for each minute, to prevent + // this test being flaky wait till the next minute if we have less than + // five seconds until the next minute. + if _, _, sec := time.Now().Clock(); sec >= 55 { + test.SleepTillNextMinute() + } + + session := loginUser(t, "user2") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser) + + publicKeyring := `-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=3agD +-----END PGP PUBLIC KEY BLOCK-----` + + privateKeyring := `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=9to/ +-----END PGP PRIVATE KEY BLOCK----- +` + block, err := armor.Decode(strings.NewReader(privateKeyring)) + require.NoError(t, err) + keyring, err := openpgp.ReadKeyRing(block.Body) + require.NoError(t, err) + assert.Len(t, keyring, 1) + + var verificationToken string + t.Run("No signature provided", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithJSON(t, "POST", "/api/v1/user/gpg_keys", api.CreateGPGKeyOption{ + ArmoredKey: publicKeyring, + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusNotFound) + + var apiError context.APIError + DecodeJSON(t, resp, &apiError) + + var ok bool + _, verificationToken, ok = strings.Cut(apiError.Message, ": ") + assert.True(t, ok) + }) + + t.Run("Signature provided", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + signatureOutput := &bytes.Buffer{} + require.NoError(t, openpgp.ArmoredDetachSign(signatureOutput, keyring[0], strings.NewReader(verificationToken), nil)) + + req := NewRequestWithJSON(t, "POST", "/api/v1/user/gpg_keys", api.CreateGPGKeyOption{ + ArmoredKey: publicKeyring, + Signature: signatureOutput.String(), + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + }) + + t.Run("{user}.gpg", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user2.gpg") + resp := session.MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, `-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsFNBGlUhjYBEADHuyzOjbPk3U0qC85lGTecJBKb1CgUZ0xphv5wNg1TBzLjQLRB +DzbnhGAROTjS08dESwrG5sf8lNKRRr9rRIKQ7p+OqhK2AkOqGC25vB6vJSBhUYNy +UWDqV02XSmNy33FlwAgalIiff12hpZ+PctmzFcBY6TJ7Zh1dIKvS17CNZvkfjb/r +m1r8VQk8I4qdgVUg8Ky898T8B6/sAHhKcDZNXXoOLpCCUsm6STCVlp+YE0fN3H6u +UiyJ++zjrS85fzV9Ug+rDZr+V/ZiOc5e+mqIo51L9m9VfFsmZfJDZvPBcFJgcISv +S7LySW/lJvZ+ntKoFANzxzlLOUNYWJv+X/tYzJIoiu4J9NAuUwieFU85CYNbz9aO +7BUlVvNDVNn2sSTX/Aum9nqLvdi17pQRNmp7NFrmSzD9GbvD1G4C51tDesnCJI/j +m5QQpor8PeiOAcaHey/GFIS0+e1nytT3RU72P8M22u44QVp+WSAeahnsEBHVLbA7 +BUoShykXul65d9Tko2H6lf4s5ggi66mXPvHH1O0Ry57JEcJVAFHpp+teckmXf8zD +a+/ha4U+RtJkkJ4RI6mPrFnamx57mYPT2ji0igpU2paAtIGhbDxSi5UD3ZycdmDj +Tw/uUTxl/lon+N7DVEFC+2F8HD2Aiw6TJ92AWs3CCZRsURMphH1W7cskvQARAQAB +zRZIYWhhIHVzZXIyQGV4YW1wbGUuY29twsGOBBMBCgA4FiEEr+MV6CFVMbWYwDdi +we3dkWyHwqYFAmlUhjYCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQwe3d +kWyHwqapNhAAlPbcf96Ntyk0vttBOSDS/owq1fk2I/h66NgDK0G3601JT0JhFv1P +nY9YV5w5rzuTvCLbQWALr8UD52B8Ua6fEn5XjiLf6dU6OuZnYqIgTqi2qm4xCuNh +p5E1UqNLBb6Nv225R1CB6XGEda+wFQkTRif2e+bQOi4/+hl6tp6tQXkqAMdJAhD5 +3v0nvvpExHkPtRyfnoP2pFLC3Oic1fVlNUD9Gz2U/rL+BkKVFTRXHfsdf1eP5osO +HbgXe8S8xWAW+K8+FeU/5uyzx6HVIMHvvu8ZxefEvkuFyBtzeikOulp7gY0OBSpr +ysRbPGUHXz2W+l3t21L3QKx++gq+aPW3ILkzyK0ovtXoJdU0QeJmbM7UaqfkYHcU +ut9HaohCheLIcWlpTaqJlqMeSxx+I0NHIbRGILM2Db0DWRms0zZxYIywtlJlolSP +hJeq3jd/Yc334CXVtY/bU8pxInJewzUtAqMhJ8AUvUfsUpmaMUg0Vkqv7NrZBhL7 +TFZXsVpw1M3Vc5nF8cfX8GfDh16cmFx9Tpz9SrVPSVAwiK1V+Lp9p6CGPZhE3Y33 +b94YUmt+CvWxKx8OeiUGEqLG3W/KpXbnYUUetyCtCMikdmaGPpvq0ifDv5AEt3eZ +DWcqZOZ1dSTG9wmKghkVrgnB6Zow4c62WFtrDtFoN0XAjC42KsufgNQ= +=766/ +-----END PGP PUBLIC KEY BLOCK-----`, resp.Body.String()) + }) +} diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go index 1d24e51dfb..0521ce7584 100644 --- a/tests/integration/api_repo_test.go +++ b/tests/integration/api_repo_test.go @@ -289,6 +289,39 @@ func TestAPIViewRepo(t *testing.T) { assert.EqualValues(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})