Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,9 @@ LEVEL = Info
;ALLOW_ONLY_EXTERNAL_REGISTRATION = false
;;
;; User must sign in to view anything.
;; It could be set to "expensive" to block anonymous users accessing some pages which consume a lot of resources,
;; for example: block anonymous AI crawlers from accessing repo code pages.
;; The "expensive" mode is experimental and subject to change.
;REQUIRE_SIGNIN_VIEW = false
;;
;; Mail notification
Expand Down
1 change: 1 addition & 0 deletions modules/setting/config_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type ConfigKey interface {
In(defaultVal string, candidates []string) string
String() string
Strings(delim string) []string
Bool() (bool, error)

MustString(defaultVal string) string
MustBool(defaultVal ...bool) bool
Expand Down
16 changes: 14 additions & 2 deletions modules/setting/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ var Service = struct {
ShowRegistrationButton bool
EnablePasswordSignInForm bool
ShowMilestonesDashboardPage bool
RequireSignInView bool
RequireSignInViewStrict bool
BlockAnonymousAccessExpensive bool
EnableNotifyMail bool
EnableBasicAuth bool
EnablePasskeyAuth bool
Expand Down Expand Up @@ -159,7 +160,18 @@ func loadServiceFrom(rootCfg ConfigProvider) {
Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST")
Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration))
Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true)
Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()

// boolean values are considered as "strict"
var err error
Service.RequireSignInViewStrict, err = sec.Key("REQUIRE_SIGNIN_VIEW").Bool()
if s := sec.Key("REQUIRE_SIGNIN_VIEW").String(); err != nil && s != "" {
// non-boolean value only supports "expensive" at the moment
Service.BlockAnonymousAccessExpensive = s == "expensive"
if !Service.BlockAnonymousAccessExpensive {
log.Fatal("Invalid config option: REQUIRE_SIGNIN_VIEW = %s", s)
}
}

Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true)
Service.EnablePasskeyAuth = sec.Key("ENABLE_PASSKEY_AUTHENTICATION").MustBool(true)
Expand Down
2 changes: 1 addition & 1 deletion routers/api/packages/cargo/cargo.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func apiError(ctx *context.Context, status int, obj any) {

// https://rust-lang.github.io/rfcs/2789-sparse-index.html
func RepositoryConfig(ctx *context.Context) {
ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInView || ctx.Package.Owner.Visibility != structs.VisibleTypePublic))
ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInViewStrict || ctx.Package.Owner.Visibility != structs.VisibleTypePublic))
}

func EnumeratePackageVersions(ctx *context.Context) {
Expand Down
4 changes: 2 additions & 2 deletions routers/api/packages/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func apiUnauthorizedError(ctx *context.Context) {

// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled)
func ReqContainerAccess(ctx *context.Context) {
if ctx.Doer == nil || (setting.Service.RequireSignInView && ctx.Doer.IsGhost()) {
if ctx.Doer == nil || (setting.Service.RequireSignInViewStrict && ctx.Doer.IsGhost()) {
apiUnauthorizedError(ctx)
}
}
Expand All @@ -152,7 +152,7 @@ func Authenticate(ctx *context.Context) {
u := ctx.Doer
packageScope := auth_service.GetAccessScope(ctx.Data)
if u == nil {
if setting.Service.RequireSignInView {
if setting.Service.RequireSignInViewStrict {
apiUnauthorizedError(ctx)
return
}
Expand Down
4 changes: 2 additions & 2 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ func reqToken() func(ctx *context.APIContext) {

func reqExploreSignIn() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if (setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned {
if (setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned {
ctx.APIError(http.StatusUnauthorized, "you must be signed in to search for users")
}
}
Expand Down Expand Up @@ -886,7 +886,7 @@ func Routes() *web.Router {
m.Use(apiAuth(buildAuthGroup()))

m.Use(verifyAuthWithOptions(&common.VerifyOptions{
SignInRequired: setting.Service.RequireSignInView,
SignInRequired: setting.Service.RequireSignInViewStrict,
}))

addActionsRoutes := func(
Expand Down
88 changes: 88 additions & 0 deletions routers/common/blockexpensive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package common

import (
"net/http"
"strings"

user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"

"github.com/go-chi/chi/v5"
)

func BlockExpensive() func(next http.Handler) http.Handler {
if !setting.Service.BlockAnonymousAccessExpensive {
return nil
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ret := determineRequestPriority(reqctx.FromContext(req.Context()))
if !ret.SignIn {
if ret.Expensive || ret.LongPolling {
http.Redirect(w, req, setting.AppSubURL+"/user/login", http.StatusSeeOther)
return
}
}
next.ServeHTTP(w, req)
})
}
}

func isRoutePathExpensive(routePattern string) bool {
if strings.HasPrefix(routePattern, "/user/") || strings.HasPrefix(routePattern, "/login/") {
return false
}

expensivePaths := []string{
// code related
"/{username}/{reponame}/archive/",
"/{username}/{reponame}/blame/",
"/{username}/{reponame}/commit/",
"/{username}/{reponame}/commits/",
"/{username}/{reponame}/media/",
"/{username}/{reponame}/raw/",
"/{username}/{reponame}/src/",

// issue & PR related (no trailing slash)
"/{username}/{reponame}/issues",
"/{username}/{reponame}/{type:issues}",
"/{username}/{reponame}/pulls",
"/{username}/{reponame}/{type:pulls}",

// wiki
"/{username}/{reponame}/wiki/",
}
for _, path := range expensivePaths {
if strings.HasPrefix(routePattern, path) {
return true
}
}
return false
}

func isRoutePathForLongPolling(routePattern string) bool {
return routePattern == "/user/events"
}

// TODO: add some tests

func determineRequestPriority(reqCtx reqctx.RequestContext) (ret struct {
SignIn bool
Expensive bool
LongPolling bool
},
) {
chiRoutePath := chi.RouteContext(reqCtx).RoutePattern()
if _, ok := reqCtx.GetData()[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
ret.SignIn = true
} else {
ret.Expensive = isRoutePathExpensive(chiRoutePath)
ret.LongPolling = isRoutePathForLongPolling(chiRoutePath)
}
return ret
}
2 changes: 1 addition & 1 deletion routers/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ func Install(ctx *context.Context) {
form.DisableRegistration = setting.Service.DisableRegistration
form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration
form.EnableCaptcha = setting.Service.EnableCaptcha
form.RequireSignInView = setting.Service.RequireSignInView
form.RequireSignInView = setting.Service.RequireSignInViewStrict
form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
Expand Down
2 changes: 1 addition & 1 deletion routers/private/serv.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ func ServCommand(ctx *context.PrivateContext) {
repo.IsPrivate ||
owner.Visibility.IsPrivate() ||
(user != nil && user.IsRestricted) || // user will be nil if the key is a deploykey
setting.Service.RequireSignInView) {
setting.Service.RequireSignInViewStrict) {
if key.Type == asymkey_model.KeyTypeDeploy {
if deployKey.Mode < mode {
ctx.JSON(http.StatusUnauthorized, private.Response{
Expand Down
2 changes: 1 addition & 1 deletion routers/web/githttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (

func addOwnerRepoGitHTTPRouters(m *web.Router) {
reqGitSignIn := func(ctx *context.Context) {
if !setting.Service.RequireSignInView {
if !setting.Service.RequireSignInViewStrict {
return
}
// rely on the results of Contexter
Expand Down
2 changes: 1 addition & 1 deletion routers/web/repo/githttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
// Only public pull don't need auth.
isPublicPull := repoExist && !repo.IsPrivate && isPull
var (
askAuth = !isPublicPull || setting.Service.RequireSignInView
askAuth = !isPublicPull || setting.Service.RequireSignInViewStrict
environ []string
)

Expand Down
16 changes: 8 additions & 8 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,23 +283,23 @@ func Routes() *web.Router {
mid = append(mid, goGet)
mid = append(mid, common.PageTmplFunctions)

others := web.NewRouter()
others.Use(mid...)
registerRoutes(others)
routes.Mount("", others)
webRoutes := web.NewRouter()
webRoutes.Use(mid...)
webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive())
routes.Mount("", webRoutes)
return routes
}

var optSignInIgnoreCsrf = verifyAuthWithOptions(&common.VerifyOptions{DisableCSRF: true})

// registerRoutes register routes
func registerRoutes(m *web.Router) {
// registerWebRoutes register routes
func registerWebRoutes(m *web.Router) {
// required to be signed in or signed out
reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true})
reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true})
// optional sign in (if signed in, use the user as doer, if not, no doer)
optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView})
optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView})
optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict})
optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView})

validation.AddBindingRules()

Expand Down
2 changes: 1 addition & 1 deletion services/context/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, any)) *Package
}

func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) {
if setting.Service.RequireSignInView && (doer == nil || doer.IsGhost()) {
if setting.Service.RequireSignInViewStrict && (doer == nil || doer.IsGhost()) {
return perm.AccessModeNone, nil
}

Expand Down
2 changes: 1 addition & 1 deletion services/packages/cargo/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ func createOrUpdateConfigFile(ctx context.Context, repo *repo_model.Repository,
"Initialize Cargo Config",
func(t *files_service.TemporaryUploadRepository) error {
var b bytes.Buffer
err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInView || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate))
err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInViewStrict || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate))
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion templates/admin/config.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
<dt>{{ctx.Locale.Tr "admin.config.enable_openid_signin"}}</dt>
<dd>{{svg (Iif .Service.EnableOpenIDSignIn "octicon-check" "octicon-x")}}</dd>
<dt>{{ctx.Locale.Tr "admin.config.require_sign_in_view"}}</dt>
<dd>{{svg (Iif .Service.RequireSignInView "octicon-check" "octicon-x")}}</dd>
<dd>{{svg (Iif .Service.RequireSignInViewStrict "octicon-check" "octicon-x")}}</dd>
<dt>{{ctx.Locale.Tr "admin.config.mail_notify"}}</dt>
<dd>{{svg (Iif .Service.EnableNotifyMail "octicon-check" "octicon-x")}}</dd>
<dt>{{ctx.Locale.Tr "admin.config.enable_captcha"}}</dt>
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/api_org_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func TestAPIOrgEditBadVisibility(t *testing.T) {

func TestAPIOrgDeny(t *testing.T) {
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.Service.RequireSignInView, true)()
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()

orgName := "user1_org"
req := NewRequestf(t, "GET", "/api/v1/orgs/%s", orgName)
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/api_packages_container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func TestPackageContainer(t *testing.T) {
AddTokenAuth(anonymousToken)
MakeRequest(t, req, http.StatusOK)

defer test.MockVariableValue(&setting.Service.RequireSignInView, true)()
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()

req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
MakeRequest(t, req, http.StatusUnauthorized)
Expand Down
7 changes: 2 additions & 5 deletions tests/integration/api_packages_generic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"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/tests"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -131,11 +132,7 @@ func TestPackageGeneric(t *testing.T) {

t.Run("RequireSignInView", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()

setting.Service.RequireSignInView = true
defer func() {
setting.Service.RequireSignInView = false
}()
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()

req = NewRequest(t, "GET", url+"/dummy.bin")
MakeRequest(t, req, http.StatusUnauthorized)
Expand Down