Merge pull request 'Allow instance-wide disabling of forking' (#2445) from algernon/forgejo:f/disable-forks into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2445
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
Earl Warren 2024-02-25 15:13:15 +00:00
commit 649ca2b230
14 changed files with 162 additions and 60 deletions

View File

@ -991,6 +991,9 @@ LEVEL = Info
;; Disable stars feature.
;DISABLE_STARS = false
;;
;; Disable repository forking.
;DISABLE_FORKS = false
;;
;; The default branch name of new repositories
;DEFAULT_BRANCH = main
;;

View File

@ -198,6 +198,7 @@ func Contexter() func(next http.Handler) http.Handler {
// FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
ctx.Data["DisableStars"] = setting.Repository.DisableStars
ctx.Data["DisableForks"] = setting.Repository.DisableForks
ctx.Data["EnableActions"] = setting.Actions.Enabled
ctx.Data["ManifestData"] = setting.ManifestData

View File

@ -50,6 +50,7 @@ var (
PrefixArchiveFiles bool
DisableMigrations bool
DisableStars bool `ini:"DISABLE_STARS"`
DisableForks bool
DefaultBranch string
AllowAdoptionOfUnadoptedRepositories bool
AllowDeleteOfUnadoptedRepositories bool
@ -172,6 +173,7 @@ var (
PrefixArchiveFiles: true,
DisableMigrations: false,
DisableStars: false,
DisableForks: false,
DefaultBranch: "main",
AllowForkWithoutMaximumLimit: true,

View File

@ -9,6 +9,7 @@ type GeneralRepoSettings struct {
HTTPGitDisabled bool `json:"http_git_disabled"`
MigrationsDisabled bool `json:"migrations_disabled"`
StarsDisabled bool `json:"stars_disabled"`
ForksDisabled bool `json:"forks_disabled"`
TimeTrackingDisabled bool `json:"time_tracking_disabled"`
LFSDisabled bool `json:"lfs_disabled"`
}

View File

@ -1161,8 +1161,10 @@ func Routes() *web.Route {
m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile)
m.Get("/media/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFileOrLFS)
m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive)
m.Combo("/forks").Get(repo.ListForks).
Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork)
if !setting.Repository.DisableForks {
m.Combo("/forks").Get(repo.ListForks).
Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork)
}
m.Group("/branches", func() {
m.Get("", repo.ListBranches)
m.Get("/*", repo.GetBranch)

View File

@ -61,6 +61,7 @@ func GetGeneralRepoSettings(ctx *context.APIContext) {
HTTPGitDisabled: setting.Repository.DisableHTTPGit,
MigrationsDisabled: setting.Repository.DisableMigrations,
StarsDisabled: setting.Repository.DisableStars,
ForksDisabled: setting.Repository.DisableForks,
TimeTrackingDisabled: !setting.Service.EnableTimetracking,
LFSDisabled: !setting.LFS.StartServer,
})

View File

@ -968,7 +968,9 @@ func registerRoutes(m *web.Route) {
m.Post("/create", web.Bind(forms.CreateRepoForm{}), repo.CreatePost)
m.Get("/migrate", repo.Migrate)
m.Post("/migrate", web.Bind(forms.MigrateRepoForm{}), repo.MigratePost)
m.Get("/fork/{repoid}", context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader, repo.ForkByID)
if !setting.Repository.DisableForks {
m.Get("/fork/{repoid}", context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader, repo.ForkByID)
}
m.Get("/search", repo.SearchRepo)
}, reqSignIn)
@ -1148,8 +1150,10 @@ func registerRoutes(m *web.Route) {
// Grouping for those endpoints that do require authentication
m.Group("/{username}/{reponame}", func() {
m.Combo("/fork", reqRepoCodeReader).Get(repo.Fork).
Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost)
if !setting.Repository.DisableForks {
m.Combo("/fork", reqRepoCodeReader).Get(repo.Fork).
Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost)
}
m.Group("/issues", func() {
m.Group("/new", func() {
m.Combo("").Get(context.RepoRef(), repo.NewIssue).
@ -1560,9 +1564,11 @@ func registerRoutes(m *web.Route) {
m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.Home)
}, repo.SetEditorconfigIfExists)
m.Group("", func() {
m.Get("/forks", repo.Forks)
}, context.RepoRef(), reqRepoCodeReader)
if !setting.Repository.DisableForks {
m.Group("", func() {
m.Get("/forks", repo.Forks)
}, context.RepoRef(), reqRepoCodeReader)
}
m.Get("/commit/{sha:([a-f0-9]{4,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff)
}, ignSignIn, context.RepoAssignment, context.UnitTypes())

View File

@ -39,7 +39,9 @@
{{if not $.DisableStars}}
<a class="text grey flex-text-inline" href="{{.Link}}/stars">{{svg "octicon-star" 16}}{{.NumStars}}</a>
{{end}}
<a class="text grey flex-text-inline" href="{{.Link}}/forks">{{svg "octicon-git-branch" 16}}{{.NumForks}}</a>
{{if not $.DisableForks}}
<a class="text grey flex-text-inline" href="{{.Link}}/forks">{{svg "octicon-git-branch" 16}}{{.NumForks}}</a>
{{end}}
</div>
</div>
{{$description := .DescriptionHTML $.Context}}

View File

@ -29,8 +29,10 @@
<a class="{{if eq .SortType "moststars"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=moststars&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.moststars"}}</a>
<a class="{{if eq .SortType "feweststars"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=feweststars&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.feweststars"}}</a>
{{end}}
<a class="{{if eq .SortType "mostforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=mostforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</a>
<a class="{{if eq .SortType "fewestforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=fewestforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</a>
{{if not .DisableForks}}
<a class="{{if eq .SortType "mostforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=mostforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</a>
<a class="{{if eq .SortType "fewestforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=fewestforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</a>
{{end}}
</div>
</div>
</div>

View File

@ -62,55 +62,8 @@
{{if not $.DisableStars}}
{{template "repo/star_unstar" $}}
{{end}}
{{if and (not .IsEmpty) ($.Permission.CanRead $.UnitTypeCode)}}
<div class="ui labeled button
{{if or (not $.IsSigned) (and (not $.CanSignedUserFork) (not $.UserAndOrgForks))}}
disabled
{{end}}"
{{if not $.IsSigned}}
data-tooltip-content="{{ctx.Locale.Tr "repo.fork_guest_user"}}"
{{else if and (not $.CanSignedUserFork) (not $.UserAndOrgForks)}}
data-tooltip-content="{{ctx.Locale.Tr "repo.fork_from_self"}}"
{{end}}
>
<a class="ui compact{{if $.ShowForkModal}} show-modal{{end}} small basic button"
{{if not $.CanSignedUserFork}}
{{if gt (len $.UserAndOrgForks) 1}}
data-modal="#fork-repo-modal"
{{else if eq (len $.UserAndOrgForks) 1}}
href="{{AppSubUrl}}/{{(index $.UserAndOrgForks 0).FullName}}"
{{/*else is not required here, because the button shouldn't link to any site if you can't create a fork*/}}
{{end}}
{{else if not $.UserAndOrgForks}}
href="{{$.RepoLink}}/fork"
{{else}}
data-modal="#fork-repo-modal"
{{end}}
>
{{svg "octicon-repo-forked"}}<span class="text">{{ctx.Locale.Tr "repo.fork"}}</span>
</a>
<div class="ui small modal" id="fork-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.already_forked" .Name}}
</div>
<div class="content gt-text-left">
<div class="ui list">
{{range $.UserAndOrgForks}}
<div class="ui item gt-py-3">
<a href="{{.Link}}">{{svg "octicon-repo-forked" 16 "gt-mr-3"}}{{.FullName}}</a>
</div>
{{end}}
</div>
{{if $.CanSignedUserFork}}
<div class="divider"></div>
<a href="{{$.RepoLink}}/fork">{{ctx.Locale.Tr "repo.fork_to_different_account"}}</a>
{{end}}
</div>
</div>
<a class="ui basic label" href="{{.Link}}/forks">
{{CountFmt .NumForks}}
</a>
</div>
{{if not $.DisableForks}}
{{template "repo/header_fork" $}}
{{end}}
</div>
{{end}}

View File

@ -0,0 +1,50 @@
{{if and (not .IsEmpty) ($.Permission.CanRead $.UnitTypeCode)}}
<div class="ui labeled button
{{if or (not $.IsSigned) (and (not $.CanSignedUserFork) (not $.UserAndOrgForks))}}
disabled
{{end}}"
{{if not $.IsSigned}}
data-tooltip-content="{{ctx.Locale.Tr "repo.fork_guest_user"}}"
{{else if and (not $.CanSignedUserFork) (not $.UserAndOrgForks)}}
data-tooltip-content="{{ctx.Locale.Tr "repo.fork_from_self"}}"
{{end}}
>
<a class="ui compact{{if $.ShowForkModal}} show-modal{{end}} small basic button"
{{if not $.CanSignedUserFork}}
{{if gt (len $.UserAndOrgForks) 1}}
data-modal="#fork-repo-modal"
{{else if eq (len $.UserAndOrgForks) 1}}
href="{{AppSubUrl}}/{{(index $.UserAndOrgForks 0).FullName}}"
{{/*else is not required here, because the button shouldn't link to any site if you can't create a fork*/}}
{{end}}
{{else if not $.UserAndOrgForks}}
href="{{$.RepoLink}}/fork"
{{else}}
data-modal="#fork-repo-modal"
{{end}}
>
{{svg "octicon-repo-forked"}}<span class="text">{{ctx.Locale.Tr "repo.fork"}}</span>
</a>
<div class="ui small modal" id="fork-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.already_forked" .Name}}
</div>
<div class="content gt-text-left">
<div class="ui list">
{{range $.UserAndOrgForks}}
<div class="ui item gt-py-3">
<a href="{{.Link}}">{{svg "octicon-repo-forked" 16 "gt-mr-3"}}{{.FullName}}</a>
</div>
{{end}}
</div>
{{if $.CanSignedUserFork}}
<div class="divider"></div>
<a href="{{$.RepoLink}}/fork">{{ctx.Locale.Tr "repo.fork_to_different_account"}}</a>
{{end}}
</div>
</div>
<a class="ui basic label" href="{{.Link}}/forks">
{{CountFmt .NumForks}}
</a>
</div>
{{end}}

View File

@ -20565,6 +20565,10 @@
"description": "GeneralRepoSettings contains global repository settings exposed by API",
"type": "object",
"properties": {
"forks_disabled": {
"type": "boolean",
"x-go-name": "ForksDisabled"
},
"http_git_disabled": {
"type": "boolean",
"x-go-name": "HTTPGitDisabled"

View File

@ -1,13 +1,18 @@
// Copyright 2017 The Gogs Authors. All rights reserved.
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"net/url"
"testing"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/tests"
)
@ -16,3 +21,27 @@ func TestCreateForkNoLogin(t *testing.T) {
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{})
MakeRequest(t, req, http.StatusUnauthorized)
}
func TestAPIDisabledForkRepo(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
defer test.MockVariableValue(&setting.Repository.DisableForks, true)()
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
t.Run("fork listing", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks")
MakeRequest(t, req, http.StatusNotFound)
})
t.Run("forking", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
session := loginUser(t, "user5")
token := getTokenForLoggedInUser(t, session)
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{}).AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusNotFound)
})
})
}

View File

@ -1,4 +1,5 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
@ -14,6 +15,9 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/routers"
repo_service "code.gitea.io/gitea/services/repository"
"code.gitea.io/gitea/tests"
@ -119,6 +123,48 @@ func TestRepoFork(t *testing.T) {
session.MakeRequest(t, req, http.StatusNotFound)
})
})
t.Run("DISABLE_FORKS", func(t *testing.T) {
defer test.MockVariableValue(&setting.Repository.DisableForks, true)()
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
t.Run("fork button not present", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// The "Fork" button should not appear on the repo home
req := NewRequest(t, "GET", "/user2/repo1")
resp := MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
htmlDoc.AssertElement(t, "[href=/user2/repo1/fork]", false)
})
t.Run("forking by URL", func(t *testing.T) {
t.Run("by name", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Forking by URL should be Not Found
req := NewRequest(t, "GET", "/user2/repo1/fork")
session.MakeRequest(t, req, http.StatusNotFound)
})
t.Run("by legacy URL", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Forking by legacy URL should be Not Found
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // user2/repo1
req := NewRequestf(t, "GET", "/repo/fork/%d", repo.ID)
session.MakeRequest(t, req, http.StatusNotFound)
})
})
t.Run("fork listing", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Listing the forks should be Not Found, too
req := NewRequest(t, "GET", "/user2/repo1/forks")
MakeRequest(t, req, http.StatusNotFound)
})
})
})
}