diff --git a/models/repo/fork.go b/models/repo/fork.go
index 07cd31c2690a9..1c75e86458b2f 100644
--- a/models/repo/fork.go
+++ b/models/repo/fork.go
@@ -54,21 +54,6 @@ func GetUserFork(ctx context.Context, repoID, userID int64) (*Repository, error)
return &forkedRepo, nil
}
-// GetForks returns all the forks of the repository
-func GetForks(ctx context.Context, repo *Repository, listOptions db.ListOptions) ([]*Repository, error) {
- sess := db.GetEngine(ctx)
-
- var forks []*Repository
- if listOptions.Page == 0 {
- forks = make([]*Repository, 0, repo.NumForks)
- } else {
- forks = make([]*Repository, 0, listOptions.PageSize)
- sess = db.SetSessionPagination(sess, &listOptions)
- }
-
- return forks, sess.Find(&forks, &Repository{ForkID: repo.ID})
-}
-
// IncrementRepoForkNum increment repository fork number
func IncrementRepoForkNum(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", repoID)
diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go
index 1bffadbf0ae42..9bed2e919723b 100644
--- a/models/repo/repo_list.go
+++ b/models/repo/repo_list.go
@@ -98,8 +98,7 @@ func (repos RepositoryList) IDs() []int64 {
return repoIDs
}
-// LoadAttributes loads the attributes for the given RepositoryList
-func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
+func (repos RepositoryList) LoadOwners(ctx context.Context) error {
if len(repos) == 0 {
return nil
}
@@ -107,10 +106,6 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
userIDs := container.FilterSlice(repos, func(repo *Repository) (int64, bool) {
return repo.OwnerID, true
})
- repoIDs := make([]int64, len(repos))
- for i := range repos {
- repoIDs[i] = repos[i].ID
- }
// Load owners.
users := make(map[int64]*user_model.User, len(userIDs))
@@ -123,12 +118,19 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
for i := range repos {
repos[i].Owner = users[repos[i].OwnerID]
}
+ return nil
+}
+
+func (repos RepositoryList) LoadLanguageStats(ctx context.Context) error {
+ if len(repos) == 0 {
+ return nil
+ }
// Load primary language.
stats := make(LanguageStatList, 0, len(repos))
if err := db.GetEngine(ctx).
Where("`is_primary` = ? AND `language` != ?", true, "other").
- In("`repo_id`", repoIDs).
+ In("`repo_id`", repos.IDs()).
Find(&stats); err != nil {
return fmt.Errorf("find primary languages: %w", err)
}
@@ -141,10 +143,18 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
}
}
}
-
return nil
}
+// LoadAttributes loads the attributes for the given RepositoryList
+func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
+ if err := repos.LoadOwners(ctx); err != nil {
+ return err
+ }
+
+ return repos.LoadLanguageStats(ctx)
+}
+
// SearchRepoOptions holds the search options
type SearchRepoOptions struct {
db.ListOptions
diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go
index a1e3c9804ba39..14a1a8d1c4a3d 100644
--- a/routers/api/v1/repo/fork.go
+++ b/routers/api/v1/repo/fork.go
@@ -55,11 +55,20 @@ func ListForks(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- forks, err := repo_model.GetForks(ctx, ctx.Repo.Repository, utils.GetListOptions(ctx))
+ forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, utils.GetListOptions(ctx))
if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetForks", err)
+ ctx.Error(http.StatusInternalServerError, "FindForks", err)
return
}
+ if err := repo_model.RepositoryList(forks).LoadOwners(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadOwners", err)
+ return
+ }
+ if err := repo_model.RepositoryList(forks).LoadUnits(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadUnits", err)
+ return
+ }
+
apiForks := make([]*api.Repository, len(forks))
for i, fork := range forks {
permission, err := access_model.GetUserRepoPermission(ctx, fork, ctx.Doer)
@@ -70,7 +79,7 @@ func ListForks(ctx *context.APIContext) {
apiForks[i] = convert.ToRepo(ctx, fork, permission)
}
- ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumForks))
+ ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, apiForks)
}
diff --git a/routers/web/auth/oauth2_provider.go b/routers/web/auth/oauth2_provider.go
index faea34959fb5b..d844d42421330 100644
--- a/routers/web/auth/oauth2_provider.go
+++ b/routers/web/auth/oauth2_provider.go
@@ -80,12 +80,12 @@ func (err errCallback) Error() string {
}
type userInfoResponse struct {
- Sub string `json:"sub"`
- Name string `json:"name"`
- Username string `json:"preferred_username"`
- Email string `json:"email"`
- Picture string `json:"picture"`
- Groups []string `json:"groups"`
+ Sub string `json:"sub"`
+ Name string `json:"name"`
+ PreferredUsername string `json:"preferred_username"`
+ Email string `json:"email"`
+ Picture string `json:"picture"`
+ Groups []string `json:"groups"`
}
// InfoOAuth manages request for userinfo endpoint
@@ -97,11 +97,11 @@ func InfoOAuth(ctx *context.Context) {
}
response := &userInfoResponse{
- Sub: fmt.Sprint(ctx.Doer.ID),
- Name: ctx.Doer.FullName,
- Username: ctx.Doer.Name,
- Email: ctx.Doer.Email,
- Picture: ctx.Doer.AvatarLink(ctx),
+ Sub: fmt.Sprint(ctx.Doer.ID),
+ Name: ctx.Doer.FullName,
+ PreferredUsername: ctx.Doer.Name,
+ Email: ctx.Doer.Email,
+ Picture: ctx.Doer.AvatarLink(ctx),
}
groups, err := oauth2_provider.GetOAuthGroupsForUser(ctx, ctx.Doer)
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 7030f6d8a982a..5d68ace29b535 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -1151,26 +1151,25 @@ func Forks(ctx *context.Context) {
if page <= 0 {
page = 1
}
+ pageSize := setting.ItemsPerPage
- pager := context.NewPagination(ctx.Repo.Repository.NumForks, setting.ItemsPerPage, page, 5)
- ctx.Data["Page"] = pager
-
- forks, err := repo_model.GetForks(ctx, ctx.Repo.Repository, db.ListOptions{
- Page: pager.Paginater.Current(),
- PageSize: setting.ItemsPerPage,
+ forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, db.ListOptions{
+ Page: page,
+ PageSize: pageSize,
})
if err != nil {
- ctx.ServerError("GetForks", err)
+ ctx.ServerError("FindForks", err)
return
}
- for _, fork := range forks {
- if err = fork.LoadOwner(ctx); err != nil {
- ctx.ServerError("LoadOwner", err)
- return
- }
+ if err := repo_model.RepositoryList(forks).LoadOwners(ctx); err != nil {
+ ctx.ServerError("LoadAttributes", err)
+ return
}
+ pager := context.NewPagination(int(total), pageSize, page, 5)
+ ctx.Data["Page"] = pager
+
ctx.Data["Forks"] = forks
ctx.HTML(http.StatusOK, tplForks)
diff --git a/services/auth/basic.go b/services/auth/basic.go
index 90bd64237091d..1f6c3a442d1d8 100644
--- a/services/auth/basic.go
+++ b/services/auth/basic.go
@@ -5,6 +5,7 @@
package auth
import (
+ "errors"
"net/http"
"strings"
@@ -141,6 +142,15 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
}
if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
+ // Check if the user has webAuthn registration
+ hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(req.Context(), u.ID)
+ if err != nil {
+ return nil, err
+ }
+ if hasWebAuthn {
+ return nil, errors.New("Basic authorization is not allowed while webAuthn enrolled")
+ }
+
if err := validateTOTP(req, u); err != nil {
return nil, err
}
diff --git a/services/repository/fork.go b/services/repository/fork.go
index 5b24015a0384f..bc4fdf85627b0 100644
--- a/services/repository/fork.go
+++ b/services/repository/fork.go
@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
@@ -20,6 +21,8 @@ import (
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
notify_service "code.gitea.io/gitea/services/notify"
+
+ "xorm.io/builder"
)
// ErrForkAlreadyExist represents a "ForkAlreadyExist" kind of error.
@@ -247,3 +250,24 @@ func ConvertForkToNormalRepository(ctx context.Context, repo *repo_model.Reposit
return err
}
+
+type findForksOptions struct {
+ db.ListOptions
+ RepoID int64
+ Doer *user_model.User
+}
+
+func (opts findForksOptions) ToConds() builder.Cond {
+ return builder.Eq{"fork_id": opts.RepoID}.And(
+ repo_model.AccessibleRepositoryCondition(opts.Doer, unit.TypeInvalid),
+ )
+}
+
+// FindForks returns all the forks of the repository
+func FindForks(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, listOptions db.ListOptions) ([]*repo_model.Repository, int64, error) {
+ return db.FindAndCount[repo_model.Repository](ctx, findForksOptions{
+ ListOptions: listOptions,
+ RepoID: repo.ID,
+ Doer: doer,
+ })
+}
diff --git a/templates/repo/forks.tmpl b/templates/repo/forks.tmpl
index 412c59b60e84d..725b67c651cb1 100644
--- a/templates/repo/forks.tmpl
+++ b/templates/repo/forks.tmpl
@@ -5,12 +5,14 @@
+
{{template "base/paginate" .}}
diff --git a/tests/integration/api_fork_test.go b/tests/integration/api_fork_test.go
index 7c231415a318a..357dd27f86888 100644
--- a/tests/integration/api_fork_test.go
+++ b/tests/integration/api_fork_test.go
@@ -7,8 +7,16 @@ import (
"net/http"
"testing"
+ "code.gitea.io/gitea/models"
+ auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
+ org_model "code.gitea.io/gitea/models/organization"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
)
func TestCreateForkNoLogin(t *testing.T) {
@@ -16,3 +24,75 @@ func TestCreateForkNoLogin(t *testing.T) {
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{})
MakeRequest(t, req, http.StatusUnauthorized)
}
+
+func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ user1Sess := loginUser(t, "user1")
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
+
+ // fork into a limited org
+ limitedOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22})
+ assert.EqualValues(t, api.VisibleTypeLimited, limitedOrg.Visibility)
+
+ ownerTeam1, err := org_model.OrgFromUser(limitedOrg).GetOwnerTeam(db.DefaultContext)
+ assert.NoError(t, err)
+ assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam1, user1))
+ user1Token := getTokenForLoggedInUser(t, user1Sess, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
+ req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{
+ Organization: &limitedOrg.Name,
+ }).AddTokenAuth(user1Token)
+ MakeRequest(t, req, http.StatusAccepted)
+
+ // fork into a private org
+ user4Sess := loginUser(t, "user4")
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user4"})
+ privateOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23})
+ assert.EqualValues(t, api.VisibleTypePrivate, privateOrg.Visibility)
+
+ ownerTeam2, err := org_model.OrgFromUser(privateOrg).GetOwnerTeam(db.DefaultContext)
+ assert.NoError(t, err)
+ assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam2, user4))
+ user4Token := getTokenForLoggedInUser(t, user4Sess, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
+ req = NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{
+ Organization: &privateOrg.Name,
+ }).AddTokenAuth(user4Token)
+ MakeRequest(t, req, http.StatusAccepted)
+
+ t.Run("Anonymous", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var forks []*api.Repository
+ DecodeJSON(t, resp, &forks)
+
+ assert.Empty(t, forks)
+ assert.EqualValues(t, "0", resp.Header().Get("X-Total-Count"))
+ })
+
+ t.Run("Logged in", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(user1Token)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var forks []*api.Repository
+ DecodeJSON(t, resp, &forks)
+
+ assert.Len(t, forks, 1)
+ assert.EqualValues(t, "1", resp.Header().Get("X-Total-Count"))
+
+ assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam2, user1))
+
+ req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(user1Token)
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ forks = []*api.Repository{}
+ DecodeJSON(t, resp, &forks)
+
+ assert.Len(t, forks, 2)
+ assert.EqualValues(t, "2", resp.Header().Get("X-Total-Count"))
+ })
+}
diff --git a/tests/integration/api_twofa_test.go b/tests/integration/api_twofa_test.go
index aad806b6dc4ff..18e6fa91b7e6c 100644
--- a/tests/integration/api_twofa_test.go
+++ b/tests/integration/api_twofa_test.go
@@ -53,3 +53,56 @@ func TestAPITwoFactor(t *testing.T) {
req.Header.Set("X-Gitea-OTP", passcode)
MakeRequest(t, req, http.StatusOK)
}
+
+func TestBasicAuthWithWebAuthn(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ // user1 has no webauthn enrolled, he can request API with basic auth
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ unittest.AssertNotExistsBean(t, &auth_model.WebAuthnCredential{UserID: user1.ID})
+ req := NewRequest(t, "GET", "/api/v1/user")
+ req.SetBasicAuth(user1.Name, "password")
+ MakeRequest(t, req, http.StatusOK)
+
+ // user1 has no webauthn enrolled, he can request git protocol with basic auth
+ req = NewRequest(t, "GET", "/user2/repo1/info/refs")
+ req.SetBasicAuth(user1.Name, "password")
+ MakeRequest(t, req, http.StatusOK)
+
+ // user1 has no webauthn enrolled, he can request container package with basic auth
+ req = NewRequest(t, "GET", "/v2/token")
+ req.SetBasicAuth(user1.Name, "password")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ type tokenResponse struct {
+ Token string `json:"token"`
+ }
+ var tokenParsed tokenResponse
+ DecodeJSON(t, resp, &tokenParsed)
+ assert.NotEmpty(t, tokenParsed.Token)
+
+ // user32 has webauthn enrolled, he can't request API with basic auth
+ user32 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 32})
+ unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{UserID: user32.ID})
+
+ req = NewRequest(t, "GET", "/api/v1/user")
+ req.SetBasicAuth(user32.Name, "notpassword")
+ resp = MakeRequest(t, req, http.StatusUnauthorized)
+
+ type userResponse struct {
+ Message string `json:"message"`
+ }
+ var userParsed userResponse
+ DecodeJSON(t, resp, &userParsed)
+ assert.EqualValues(t, "Basic authorization is not allowed while webAuthn enrolled", userParsed.Message)
+
+ // user32 has webauthn enrolled, he can't request git protocol with basic auth
+ req = NewRequest(t, "GET", "/user2/repo1/info/refs")
+ req.SetBasicAuth(user32.Name, "notpassword")
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ // user32 has webauthn enrolled, he can't request container package with basic auth
+ req = NewRequest(t, "GET", "/v2/token")
+ req.SetBasicAuth(user1.Name, "notpassword")
+ MakeRequest(t, req, http.StatusUnauthorized)
+}
diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go
index feebebf062081..52b55888b9dd9 100644
--- a/tests/integration/repo_fork_test.go
+++ b/tests/integration/repo_fork_test.go
@@ -9,8 +9,12 @@ import (
"net/http/httptest"
"testing"
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ org_model "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@@ -74,3 +78,51 @@ func TestRepoForkToOrg(t *testing.T) {
_, exists := htmlDoc.doc.Find(`a.ui.button[href*="/fork"]`).Attr("href")
assert.False(t, exists, "Forking should not be allowed anymore")
}
+
+func TestForkListLimitedAndPrivateRepos(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ forkItemSelector := ".repo-fork-item"
+
+ user1Sess := loginUser(t, "user1")
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
+
+ // fork to a limited org
+ limitedOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22})
+ assert.EqualValues(t, structs.VisibleTypeLimited, limitedOrg.Visibility)
+ ownerTeam1, err := org_model.OrgFromUser(limitedOrg).GetOwnerTeam(db.DefaultContext)
+ assert.NoError(t, err)
+ assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam1, user1))
+ testRepoFork(t, user1Sess, "user2", "repo1", limitedOrg.Name, "repo1", "")
+
+ // fork to a private org
+ user4Sess := loginUser(t, "user4")
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user4"})
+ privateOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23})
+ assert.EqualValues(t, structs.VisibleTypePrivate, privateOrg.Visibility)
+ ownerTeam2, err := org_model.OrgFromUser(privateOrg).GetOwnerTeam(db.DefaultContext)
+ assert.NoError(t, err)
+ assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam2, user4))
+ testRepoFork(t, user4Sess, "user2", "repo1", privateOrg.Name, "repo1", "")
+
+ t.Run("Anonymous", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ req := NewRequest(t, "GET", "/user2/repo1/forks")
+ resp := MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ assert.EqualValues(t, 0, htmlDoc.Find(forkItemSelector).Length())
+ })
+
+ t.Run("Logged in", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", "/user2/repo1/forks")
+ resp := user1Sess.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ assert.EqualValues(t, 1, htmlDoc.Find(forkItemSelector).Length())
+
+ assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam2, user1))
+ resp = user1Sess.MakeRequest(t, req, http.StatusOK)
+ htmlDoc = NewHTMLParser(t, resp.Body)
+ assert.EqualValues(t, 2, htmlDoc.Find(forkItemSelector).Length())
+ })
+}