From a126477e866c5d28b3dbe51f5e3ce97dffcbf199 Mon Sep 17 00:00:00 2001
From: George Tsiamasiotis <gtsiam@windowslive.com>
Date: Tue, 26 Nov 2024 08:51:51 +0200
Subject: [PATCH] feat: Add option to disable builtin authentication.

Setting ENABLE_INTERNAL_SIGNIN to false will disable the built-in
signin form, should the administrator prefer to limit users to SSO.

Continuation of forgejo/forgejo#6076
---
 custom/conf/app.example.ini              |  3 ++
 modules/setting/service.go               |  2 ++
 routers/web/auth/auth.go                 |  8 +++++
 templates/user/auth/oauth_container.tmpl |  2 ++
 templates/user/auth/signin_inner.tmpl    |  2 ++
 tests/integration/signin_test.go         | 42 ++++++++++++++++++++++++
 6 files changed, 59 insertions(+)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 7d508daa40..45b094d99c 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -901,6 +901,9 @@ LEVEL = Info
 ;; Show Registration button
 ;SHOW_REGISTRATION_BUTTON = true
 ;;
+;; Whether to allow internal signin
+; ENABLE_INTERNAL_SIGNIN = true
+;;
 ;; Show milestones dashboard page - a view of all the user's milestones
 ;SHOW_MILESTONES_DASHBOARD_PAGE = true
 ;;
diff --git a/modules/setting/service.go b/modules/setting/service.go
index e630fe85b8..5a6cc254e0 100644
--- a/modules/setting/service.go
+++ b/modules/setting/service.go
@@ -43,6 +43,7 @@ var Service = struct {
 	AllowOnlyInternalRegistration           bool
 	AllowOnlyExternalRegistration           bool
 	ShowRegistrationButton                  bool
+	EnableInternalSignIn                    bool
 	ShowMilestonesDashboardPage             bool
 	RequireSignInView                       bool
 	EnableNotifyMail                        bool
@@ -175,6 +176,7 @@ func loadServiceFrom(rootCfg ConfigProvider) {
 		Service.EmailDomainBlockList = append(Service.EmailDomainBlockList, toAdd...)
 	}
 	Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration))
+	Service.EnableInternalSignIn = sec.Key("ENABLE_INTERNAL_SIGNIN").MustBool(true)
 	Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true)
 	Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
 	Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 941586db72..71d7b8ca11 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -164,6 +164,7 @@ func SignIn(ctx *context.Context) {
 	ctx.Data["PageIsSignIn"] = true
 	ctx.Data["PageIsLogin"] = true
 	ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)
+	ctx.Data["EnableInternalSignIn"] = setting.Service.EnableInternalSignIn
 
 	if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin {
 		context.SetCaptchaData(ctx)
@@ -187,6 +188,13 @@ func SignInPost(ctx *context.Context) {
 	ctx.Data["PageIsSignIn"] = true
 	ctx.Data["PageIsLogin"] = true
 	ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)
+	ctx.Data["EnableInternalSignIn"] = setting.Service.EnableInternalSignIn
+
+	// Permission denied if EnableInternalSignIn is false
+	if !setting.Service.EnableInternalSignIn {
+		ctx.Error(http.StatusForbidden)
+		return
+	}
 
 	if ctx.HasError() {
 		ctx.HTML(http.StatusOK, tplSignIn)
diff --git a/templates/user/auth/oauth_container.tmpl b/templates/user/auth/oauth_container.tmpl
index bb6a10d408..ecae2bbbf3 100644
--- a/templates/user/auth/oauth_container.tmpl
+++ b/templates/user/auth/oauth_container.tmpl
@@ -1,7 +1,9 @@
 {{if or .OAuth2Providers .EnableOpenIDSignIn}}
+{{if .EnableInternalSignIn}}
 <div class="divider divider-text">
 	{{ctx.Locale.Tr "sign_in_or"}}
 </div>
+{{end}}
 <div id="oauth2-login-navigator" class="tw-py-1">
 	<div class="tw-flex tw-flex-col tw-justify-center">
 		<div id="oauth2-login-navigator-inner" class="tw-flex tw-flex-col tw-flex-wrap tw-items-center tw-gap-2">
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
index 56532f4b98..ddef34f35d 100644
--- a/templates/user/auth/signin_inner.tmpl
+++ b/templates/user/auth/signin_inner.tmpl
@@ -10,6 +10,7 @@
 		{{end}}
 	</h4>
 	<div class="ui attached segment">
+		{{if .EnableInternalSignIn}}
 		<form class="ui form" action="{{.SignInLink}}" method="post">
 			{{.CsrfTokenHtml}}
 			<div class="required field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
@@ -43,6 +44,7 @@
 				</button>
 			</div>
 		</form>
+		{{end}}
 
 		{{template "user/auth/oauth_container" .}}
 	</div>
diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go
index 77e19bba96..c986b844f0 100644
--- a/tests/integration/signin_test.go
+++ b/tests/integration/signin_test.go
@@ -12,6 +12,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/modules/translation"
 	"code.gitea.io/gitea/tests"
 
@@ -93,3 +94,44 @@ func TestSigninWithRememberMe(t *testing.T) {
 	req = NewRequest(t, "GET", "/user/settings")
 	session.MakeRequest(t, req, http.StatusOK)
 }
+
+func TestDisableSignin(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	t.Run("Disabled", func(t *testing.T) {
+		defer test.MockVariableValue(&setting.Service.EnableInternalSignIn, false)()
+
+		t.Run("UI", func(t *testing.T) {
+			defer tests.PrintCurrentTest(t)()
+
+			req := NewRequest(t, "GET", "/user/login")
+			resp := MakeRequest(t, req, http.StatusOK)
+			htmlDoc := NewHTMLParser(t, resp.Body)
+			htmlDoc.AssertElement(t, "form[action='/user/login']", false)
+		})
+
+		t.Run("Signin", func(t *testing.T) {
+			defer tests.PrintCurrentTest(t)()
+			req := NewRequest(t, "POST", "/user/login")
+			MakeRequest(t, req, http.StatusForbidden)
+		})
+	})
+
+	t.Run("Enabled", func(t *testing.T) {
+		defer test.MockVariableValue(&setting.Service.EnableInternalSignIn, true)()
+
+		t.Run("UI", func(t *testing.T) {
+			defer tests.PrintCurrentTest(t)()
+
+			req := NewRequest(t, "GET", "/user/login")
+			resp := MakeRequest(t, req, http.StatusOK)
+			htmlDoc := NewHTMLParser(t, resp.Body)
+			htmlDoc.AssertElement(t, "form[action='/user/login']", true)
+		})
+
+		t.Run("Signin", func(t *testing.T) {
+			defer tests.PrintCurrentTest(t)()
+			req := NewRequest(t, "POST", "/user/login")
+			MakeRequest(t, req, http.StatusOK)
+		})
+	})
+}