mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
[v11.0/forgejo] 2026-05-12 security patches (#12495)
- fix: prevent git write to wiki repo from unauthorized user via git HTTP - fix: prevent LFS authorization token from being used for read/write access after user's access is restricted from Forgejo - fix: prevent scoped API access (OAuth tokens, Access tokens) from accessing resources beyond their permitted scope via non-API endpoints (e.g. /user/repo/raw/...) - fix: implementing missing OAuth validation checks, improve protections against race conditions - fix: prevent OAuth redirect URI spoofing via non-ascii case collision - fix: strengthen Actions Artifact V4 signature algorithm against spoofing attacks - fix: update Go toolchain to 1.25.10 Co-authored-by: Derzsi Dániel <daniel@tohka.us> Co-authored-by: jvoisin <julien.voisin@dustri.org> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12495
This commit is contained in:
parent
67790f5736
commit
9e51a55b63
16 changed files with 714 additions and 40 deletions
|
|
@ -13,9 +13,13 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
unit_model "forgejo.org/models/unit"
|
||||
"forgejo.org/models/unittest"
|
||||
"forgejo.org/modules/storage"
|
||||
"forgejo.org/modules/test"
|
||||
repo_service "forgejo.org/services/repository"
|
||||
"forgejo.org/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -133,3 +137,128 @@ func TestGetAttachment(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Access under `/attachments/{uuid}` and `/{user}/{repo}/attachments/{uuid}` is permitted for API tokens. Those API
|
||||
// tokens then need to have the read:issue or read:repository and the correct resource scopes to permit access, though.
|
||||
func TestGetAttachmentViaAPITokens(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("tests/integration/fixtures/TestGetAttachmentViaAPITokens")()
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// Create attachment data for an attachment added by this test's fixture.
|
||||
_, err := storage.Attachments.Save(repo_model.AttachmentRelativePath("d962b49e-e32a-4b72-922d-33b551b629e2"), strings.NewReader("hello universe"), -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Enable Issues unit on repo 16, one of our test targets.
|
||||
repo_service.UpdateRepositoryUnits(t.Context(),
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}),
|
||||
[]repo_model.RepoUnit{{
|
||||
RepoID: 16,
|
||||
Type: unit_model.TypeIssues,
|
||||
}}, nil)
|
||||
|
||||
t.Run("attachments", func(t *testing.T) {
|
||||
t.Run("no read:issue scope", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
allToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadMisc)
|
||||
|
||||
t.Run("denied public repo1", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11").AddTokenAuth(allToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
t.Run("denied private repo2", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12").AddTokenAuth(allToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
// repo16 is a second repo used in fine-grain testing below, so we include it in other tests as a baseline
|
||||
t.Run("denied private repo16", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/attachments/d962b49e-e32a-4b72-922d-33b551b629e2").AddTokenAuth(allToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("all access token", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
allToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
|
||||
|
||||
t.Run("allowed public repo1", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11").AddTokenAuth(allToken)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "hello world", resp.Body.String())
|
||||
})
|
||||
t.Run("allowed private repo2", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12").AddTokenAuth(allToken)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "hello world", resp.Body.String())
|
||||
})
|
||||
// repo16 is a second repo used in fine-grain testing below, so we include it in other tests as a baseline
|
||||
t.Run("allowed private repo16", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/attachments/d962b49e-e32a-4b72-922d-33b551b629e2").AddTokenAuth(allToken)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "hello universe", resp.Body.String())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("user-repo-attachments", func(t *testing.T) {
|
||||
t.Run("no read:issue scope", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
allToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadMisc)
|
||||
|
||||
t.Run("denied public repo1", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/user2/repo1/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11").AddTokenAuth(allToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
t.Run("denied private repo2", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/user2/repo2/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12").AddTokenAuth(allToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
// repo16 is a second repo used in fine-grain testing below, so we include it in other tests as a baseline
|
||||
t.Run("denied private repo16", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/user2/repo16/attachments/d962b49e-e32a-4b72-922d-33b551b629e2").AddTokenAuth(allToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("all access token", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
allToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
|
||||
|
||||
t.Run("allowed public repo1", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/user2/repo1/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11").AddTokenAuth(allToken)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "hello world", resp.Body.String())
|
||||
})
|
||||
t.Run("allowed private repo2", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/user2/repo2/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12").AddTokenAuth(allToken)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "hello world", resp.Body.String())
|
||||
})
|
||||
// repo16 is a second repo used in fine-grain testing below, so we include it in other tests as a baseline
|
||||
t.Run("allowed private repo16", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/user2/repo16/attachments/d962b49e-e32a-4b72-922d-33b551b629e2").AddTokenAuth(allToken)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "hello universe", resp.Body.String())
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/tests"
|
||||
|
||||
|
|
@ -15,7 +16,6 @@ import (
|
|||
|
||||
func TestDownloadByID(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
|
||||
// Request raw blob
|
||||
|
|
@ -91,3 +91,59 @@ func TestDownloadRawTextFileWithMimeTypeMapping(t *testing.T) {
|
|||
delete(setting.MimeTypeMap.Map, ".xml")
|
||||
setting.MimeTypeMap.Enabled = false
|
||||
}
|
||||
|
||||
// Access under `/raw` is permitted for API tokens. Those API tokens then need to have the read:repository scope to
|
||||
// permit access, though. The below series of tests covers the middleware combinations on the entire `/user/repo/raw/*`
|
||||
// URL tree as they use a common middleware implementation.
|
||||
func TestDownloadAccessViaAPITokens(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
t.Run("no read:repository scope", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
allToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadMisc)
|
||||
|
||||
t.Run("denied public repo1", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/user2/repo1/raw/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f").AddTokenAuth(allToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
t.Run("denied private repo2", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/user2/repo2/raw/blob/1032bbf17fbc0d9c95bb5418dabe8f8c99278700").AddTokenAuth(allToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
t.Run("denied private repo16", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/user2/repo16/raw/blob/69554a64c1e6030f051e5c3f94bfbd773cd6a324").AddTokenAuth(allToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("all access token", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
allToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
t.Run("allowed public repo1", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/user2/repo1/raw/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f").AddTokenAuth(allToken)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String())
|
||||
})
|
||||
t.Run("allowed private repo2", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/user2/repo2/raw/blob/1032bbf17fbc0d9c95bb5418dabe8f8c99278700").AddTokenAuth(allToken)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "tree ba1aed4e2ea2443d76cec241b96be4ec990852ec\nparent 205ac761f3326a7ebe416e8673760016450b5cec\nauthor Jimmy Praet <jimmy.praet@telenet.be> 1624996449 +0200\ncommitter Jimmy Praet <jimmy.praet@telenet.be> 1624996449 +0200\n\nAdd test.xml\n", resp.Body.String())
|
||||
})
|
||||
t.Run("allowed private repo16", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/user2/repo16/raw/blob/69554a64c1e6030f051e5c3f94bfbd773cd6a324").AddTokenAuth(allToken)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "tree 24f83a471f77579fea57bac7255d6e64e70fce1c\nparent 27566bd5738fc8b4e3fef3c5e72cce608537bd95\nauthor User2 <user2@example.com> 1502042309 +0200\ncommitter User2 <user2@example.com> 1502042309 +0200\n\nnot signed commit\n", resp.Body.String())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
-
|
||||
id: 50
|
||||
uuid: d962b49e-e32a-4b72-922d-33b551b629e2
|
||||
repo_id: 16
|
||||
issue_id: 50
|
||||
release_id: 0
|
||||
uploader_id: 0
|
||||
comment_id: 0
|
||||
name: attach1
|
||||
download_count: 0
|
||||
size: 0
|
||||
created_unix: 946684800
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
-
|
||||
id: 50
|
||||
repo_id: 16
|
||||
index: 1
|
||||
poster_id: 1
|
||||
original_author_id: 0
|
||||
name: issue1
|
||||
content: content for the first issue
|
||||
milestone_id: 0
|
||||
priority: 0
|
||||
is_closed: false
|
||||
is_pull: false
|
||||
num_comments: 3
|
||||
created_unix: 946684800
|
||||
updated_unix: 978307200
|
||||
is_locked: false
|
||||
|
|
@ -194,12 +194,45 @@ func TestAccessTokenExchange(t *testing.T) {
|
|||
assert.Greater(t, len(parsed.RefreshToken), 10)
|
||||
}
|
||||
|
||||
func TestAccessTokenExchangeRedirectURIMismatch(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// The auth code fixture has redirect_uri="a", but we send a different
|
||||
// URI that is registered with the app ("https://example.com/xyzzy").
|
||||
// Per RFC 6749 §4.1.3, this must be rejected.
|
||||
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
|
||||
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
|
||||
"redirect_uri": "https://example.com/xyzzy",
|
||||
"code": "authcode",
|
||||
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
|
||||
})
|
||||
resp := MakeRequest(t, req, http.StatusBadRequest)
|
||||
|
||||
var parsedError auth.AccessTokenError
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &parsedError))
|
||||
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
|
||||
assert.Equal(t, "redirect_uri does not match the authorization request", parsedError.ErrorDescription)
|
||||
|
||||
// Using the correct redirect_uri ("a") should succeed
|
||||
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
|
||||
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
|
||||
"redirect_uri": "a",
|
||||
"code": "authcode",
|
||||
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
|
||||
})
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestAccessTokenExchangeWithPublicClient(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": "ce5a1322-42a7-11ed-b878-0242ac120002",
|
||||
"redirect_uri": "http://127.0.0.1",
|
||||
"redirect_uri": "http://127.0.0.1/",
|
||||
"code": "authcodepublic",
|
||||
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
|
||||
})
|
||||
|
|
@ -494,6 +527,56 @@ func TestRefreshTokenInvalidation(t *testing.T) {
|
|||
assert.Equal(t, "token was already used", parsedError.ErrorDescription)
|
||||
}
|
||||
|
||||
func TestRefreshTokenCrossClientUsage(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// Step 1: Obtain a refresh token via app 1 (confidential client)
|
||||
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
|
||||
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
|
||||
"redirect_uri": "a",
|
||||
"code": "authcode",
|
||||
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
|
||||
})
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
type tokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
parsed := new(tokenResponse)
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
|
||||
assert.NotEmpty(t, parsed.RefreshToken)
|
||||
|
||||
// Step 2: Try to use the refresh token with app 2 (different client): must fail
|
||||
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||
"grant_type": "refresh_token",
|
||||
"client_id": "ce5a1322-42a7-11ed-b878-0242ac120002",
|
||||
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
|
||||
"redirect_uri": "b",
|
||||
"refresh_token": parsed.RefreshToken,
|
||||
})
|
||||
resp = MakeRequest(t, req, http.StatusBadRequest)
|
||||
|
||||
var parsedError auth.AccessTokenError
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &parsedError))
|
||||
assert.Equal(t, "invalid_grant", string(parsedError.ErrorCode))
|
||||
assert.Equal(t, "refresh token was not issued to this client", parsedError.ErrorDescription)
|
||||
|
||||
// Step 3: Using the refresh token with the correct app 1 should still work
|
||||
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||
"grant_type": "refresh_token",
|
||||
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
|
||||
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
|
||||
"redirect_uri": "a",
|
||||
"refresh_token": parsed.RefreshToken,
|
||||
})
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestSignInOAuthCallbackSignIn(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
|
|
|
|||
|
|
@ -4,19 +4,29 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
unit_model "forgejo.org/models/unit"
|
||||
"forgejo.org/models/unittest"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/git"
|
||||
"forgejo.org/modules/optional"
|
||||
api "forgejo.org/modules/structs"
|
||||
"forgejo.org/modules/util"
|
||||
"forgejo.org/tests"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -24,7 +34,16 @@ import (
|
|||
func assertFileExist(t *testing.T, p string) {
|
||||
exist, err := util.IsExist(p)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
if !assert.True(t, exist) {
|
||||
dir := filepath.Dir(p)
|
||||
t.Logf("Listing files that were present in dir path %s", dir)
|
||||
entries, err := os.ReadDir(dir)
|
||||
require.NoError(t, err)
|
||||
for _, e := range entries {
|
||||
t.Logf("file in path %s -> %s", dir, e.Name())
|
||||
}
|
||||
t.Logf("End of %d entries in directory %s", len(entries), dir)
|
||||
}
|
||||
}
|
||||
|
||||
func assertFileEqual(t *testing.T, p string, content []byte) {
|
||||
|
|
@ -33,24 +52,156 @@ func assertFileEqual(t *testing.T, p string, content []byte) {
|
|||
assert.EqualValues(t, content, bs)
|
||||
}
|
||||
|
||||
func TestRepoCloneWiki(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
dstPath := t.TempDir()
|
||||
type (
|
||||
RepoWikiMethod string
|
||||
RepoWikiAuth string
|
||||
RepoWikiTarget string
|
||||
RepoWikiOperation string
|
||||
)
|
||||
|
||||
r := fmt.Sprintf("%suser2/repo1.wiki.git", u.String())
|
||||
u, _ = url.Parse(r)
|
||||
u.User = url.UserPassword("user2", userPassword)
|
||||
t.Run("Clone", func(t *testing.T) {
|
||||
require.NoError(t, git.CloneWithArgs(t.Context(), git.AllowLFSFiltersArgs(), u.String(), dstPath, git.CloneRepoOptions{}))
|
||||
assertFileEqual(t, filepath.Join(dstPath, "Home.md"), []byte("# Home page\n\nThis is the home page!\n"))
|
||||
assertFileExist(t, filepath.Join(dstPath, "Page-With-Image.md"))
|
||||
assertFileExist(t, filepath.Join(dstPath, "Page-With-Spaced-Name.md"))
|
||||
assertFileExist(t, filepath.Join(dstPath, "images"))
|
||||
assertFileExist(t, filepath.Join(dstPath, "jpeg.jpg"))
|
||||
})
|
||||
const (
|
||||
RepoWikiSSH RepoWikiMethod = "SSH"
|
||||
RepoWikiHTTP RepoWikiMethod = "HTTP"
|
||||
|
||||
RepoWikiAnonymous RepoWikiAuth = "Anonymous"
|
||||
RepoWikiAuthenticated RepoWikiAuth = "Authenticated"
|
||||
RepoWikiAuthenticatedNonOwnerUser RepoWikiAuth = "Authenticated-NonOwner"
|
||||
|
||||
RepoWikiPublic RepoWikiTarget = "Public"
|
||||
RepoWikiPrivate RepoWikiTarget = "Private"
|
||||
|
||||
RepoWikiRead RepoWikiOperation = "Read"
|
||||
RepoWikiWrite RepoWikiOperation = "Write"
|
||||
)
|
||||
|
||||
func TestRepoWikiGitOperation(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
for _, method := range []RepoWikiMethod{RepoWikiSSH, RepoWikiHTTP} {
|
||||
for _, auth := range []RepoWikiAuth{RepoWikiAnonymous, RepoWikiAuthenticated, RepoWikiAuthenticatedNonOwnerUser} {
|
||||
for _, target := range []RepoWikiTarget{RepoWikiPublic, RepoWikiPrivate} {
|
||||
for _, operation := range []RepoWikiOperation{RepoWikiRead, RepoWikiWrite} {
|
||||
t.Run(fmt.Sprintf("%s/%s/%s/%s", method, auth, target, operation), func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
doRepoWikiGitOperation(t, u, method, auth, target, operation)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func doRepoWikiGitOperation(t *testing.T, serverURL *url.URL, method RepoWikiMethod, auth RepoWikiAuth, target RepoWikiTarget, operation RepoWikiOperation) {
|
||||
repo := "repo1"
|
||||
if target == RepoWikiPrivate {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
privateRepo, _, reset := tests.CreateDeclarativeRepoWithOptions(t, user2, tests.DeclarativeRepoOptions{
|
||||
IsPrivate: optional.Some(true),
|
||||
EnabledUnits: optional.Some([]unit_model.Type{unit_model.TypeWiki}),
|
||||
})
|
||||
defer reset()
|
||||
|
||||
session := loginUser(t, user2.LoginName)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/new", user2.LoginName, privateRepo.Name)
|
||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateWikiPageOptions{
|
||||
Title: "Page With Image",
|
||||
ContentBase64: base64.StdEncoding.EncodeToString([]byte("# Page With Image\n\n\n")),
|
||||
Message: "",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
repo = privateRepo.Name
|
||||
}
|
||||
|
||||
dstPath := t.TempDir()
|
||||
r := fmt.Sprintf("%suser2/%s.wiki.git", serverURL.String(), repo)
|
||||
testURL, err := url.Parse(r)
|
||||
require.NoError(t, err)
|
||||
|
||||
if method == RepoWikiHTTP {
|
||||
switch auth {
|
||||
case RepoWikiAnonymous:
|
||||
// no-op
|
||||
case RepoWikiAuthenticated:
|
||||
testURL.User = url.UserPassword("user2", userPassword)
|
||||
case RepoWikiAuthenticatedNonOwnerUser:
|
||||
testURL.User = url.UserPassword("user20", userPassword)
|
||||
default:
|
||||
t.Fatalf("unexpected auth = %s", auth)
|
||||
}
|
||||
|
||||
doRepoWikiGitOperationInner(t, testURL, dstPath, auth, target, operation)
|
||||
} else if method == RepoWikiSSH {
|
||||
var user string
|
||||
switch auth {
|
||||
case RepoWikiAnonymous:
|
||||
t.Skip() // anonymous ssh is not supported
|
||||
case RepoWikiAuthenticated:
|
||||
user = "user2" // owner of the repo
|
||||
case RepoWikiAuthenticatedNonOwnerUser:
|
||||
user = "user20" // not the owner of the repo, not a collaborator
|
||||
default:
|
||||
t.Fatalf("unexpected auth = %s", auth)
|
||||
}
|
||||
|
||||
keyname := "my-testing-key"
|
||||
withKeyFile(t, keyname, func(keyFile string) {
|
||||
baseAPITestContext := NewAPITestContext(t, user, repo, auth_model.AccessTokenScopeWriteUser)
|
||||
t.Run("CreateUserKey", doAPICreateUserKey(baseAPITestContext, fmt.Sprintf("test-key-%s", uuid.New().String()), keyFile, func(t *testing.T, pk api.PublicKey) {}))
|
||||
|
||||
baseAPITestContext.Username = "user2" // target repo owner to compose URLs
|
||||
baseAPITestContext.Reponame = fmt.Sprintf("%s.wiki", repo)
|
||||
testURL = createSSHUrl(baseAPITestContext.GitPath(), testURL)
|
||||
|
||||
doRepoWikiGitOperationInner(t, testURL, dstPath, auth, target, operation)
|
||||
})
|
||||
} else {
|
||||
t.Fatalf("unexpected method = %s", method)
|
||||
}
|
||||
}
|
||||
|
||||
func doRepoWikiGitOperationInner(t *testing.T, gitURL *url.URL, dstPath string, auth RepoWikiAuth, target RepoWikiTarget, operation RepoWikiOperation) {
|
||||
err := git.CloneWithArgs(t.Context(), git.AllowLFSFiltersArgs(), gitURL.String(), dstPath, git.CloneRepoOptions{})
|
||||
if target == RepoWikiPrivate && (auth == RepoWikiAnonymous || auth == RepoWikiAuthenticatedNonOwnerUser) {
|
||||
require.Error(t, err, "clone must fail; auth %s shouldn't be able to access private repo")
|
||||
return // no other test conditions to satisfy if the clone failed
|
||||
}
|
||||
require.NoError(t, err, "clone must succeed; auth %s should be able to access a public repo")
|
||||
|
||||
assertFileExist(t, filepath.Join(dstPath, "Page-With-Image.md"))
|
||||
assertFileEqual(t, filepath.Join(dstPath, "Page-With-Image.md"), []byte("# Page With Image\n\n\n"))
|
||||
|
||||
if operation == RepoWikiWrite {
|
||||
f, err := os.OpenFile(filepath.Join(dstPath, "Home.md"), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o644)
|
||||
defer f.Close()
|
||||
require.NoError(t, err)
|
||||
_, err = io.WriteString(f, fmt.Sprintf("# Home Page Edited!\n%s", uuid.New().String()))
|
||||
require.NoError(t, err)
|
||||
err = f.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = git.AddChanges(dstPath, true)
|
||||
require.NoError(t, err)
|
||||
err = git.CommitChanges(dstPath, git.CommitChangesOptions{Message: "Changes made!"})
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := git.NewCommand(t.Context())
|
||||
cmd.AddArguments("push")
|
||||
cmd.AddDynamicArguments(gitURL.String())
|
||||
|
||||
stdout, stderr, err := cmd.RunStdString(&git.RunOpts{
|
||||
Dir: dstPath,
|
||||
Timeout: 2 * time.Second,
|
||||
})
|
||||
if auth == RepoWikiAuthenticated {
|
||||
require.NoError(t, err, "stdout = %q, stderr = %q", stdout, stderr)
|
||||
} else {
|
||||
require.Error(t, err, "push must fail as authentication mode %s doesn't allow write, but succeeded. stdout = %q, stderr = %q", auth, stdout, stderr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_RepoWikiPages(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue