From 159bc8842aaf05c3e238fc24bd347f48b9258979 Mon Sep 17 00:00:00 2001
From: zeripath <art27@cantab.net>
Date: Wed, 19 May 2021 15:42:36 +0100
Subject: [PATCH] Restore PAM user autocreation functionality (#15825) (#15867)

Backport #15825

* Restore PAM user autocreation functionality

PAM autoregistration of users currently fails due to email invalidity.
This PR adds a new setting to PAM to allow an email domain to be set
or just sets the email to the noreply address and if that fails falls
back to uuid@localhost

Fix #15702

Signed-off-by: Andrew Thornton <art27@cantab.net>

* As per KN4CKER

Signed-off-by: Andrew Thornton <art27@cantab.net>

Co-authored-by: 6543 <6543@obermui.de>
---
 models/login_source.go          | 15 ++++++++++++++-
 modules/forms/auth_form.go      |  1 +
 options/locale/locale_en-US.ini |  1 +
 routers/admin/auths.go          |  2 ++
 templates/admin/auth/edit.tmpl  |  4 ++++
 templates/admin/auth/new.tmpl   |  2 ++
 6 files changed, 24 insertions(+), 1 deletion(-)

diff --git a/models/login_source.go b/models/login_source.go
index fd977e20a5..57b1d56bb2 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -21,6 +21,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
+	gouuid "github.com/google/uuid"
 	jsoniter "github.com/json-iterator/go"
 
 	"xorm.io/xorm"
@@ -116,6 +117,7 @@ func (cfg *SMTPConfig) ToDB() ([]byte, error) {
 // PAMConfig holds configuration for the PAM login source.
 type PAMConfig struct {
 	ServiceName string // pam service (e.g. system-auth)
+	EmailDomain string
 }
 
 // FromDB fills up a PAMConfig from serialized format.
@@ -696,15 +698,26 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon
 
 	// Allow PAM sources with `@` in their name, like from Active Directory
 	username := pamLogin
+	email := pamLogin
 	idx := strings.Index(pamLogin, "@")
 	if idx > -1 {
 		username = pamLogin[:idx]
 	}
+	if ValidateEmail(email) != nil {
+		if cfg.EmailDomain != "" {
+			email = fmt.Sprintf("%s@%s", username, cfg.EmailDomain)
+		} else {
+			email = fmt.Sprintf("%s@%s", username, setting.Service.NoReplyAddress)
+		}
+		if ValidateEmail(email) != nil {
+			email = gouuid.New().String() + "@localhost"
+		}
+	}
 
 	user = &User{
 		LowerName:   strings.ToLower(username),
 		Name:        username,
-		Email:       pamLogin,
+		Email:       email,
 		Passwd:      password,
 		LoginType:   LoginPAM,
 		LoginSource: sourceID,
diff --git a/modules/forms/auth_form.go b/modules/forms/auth_form.go
index 7cf6b9fcd5..30621cadff 100644
--- a/modules/forms/auth_form.go
+++ b/modules/forms/auth_form.go
@@ -51,6 +51,7 @@ type AuthenticationForm struct {
 	TLS                           bool
 	SkipVerify                    bool
 	PAMServiceName                string
+	PAMEmailDomain                string
 	Oauth2Provider                string
 	Oauth2Key                     string
 	Oauth2Secret                  string
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index e679e1e874..a2320a20ed 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -2281,6 +2281,7 @@ auths.allowed_domains_helper = Leave empty to allow all domains. Separate multip
 auths.enable_tls = Enable TLS Encryption
 auths.skip_tls_verify = Skip TLS Verify
 auths.pam_service_name = PAM Service Name
+auths.pam_email_domain = PAM Email Domain (optional)
 auths.oauth2_provider = OAuth2 Provider
 auths.oauth2_icon_url = Icon URL
 auths.oauth2_clientID = Client ID (Key)
diff --git a/routers/admin/auths.go b/routers/admin/auths.go
index 12d0a2ccfa..f96c810d36 100644
--- a/routers/admin/auths.go
+++ b/routers/admin/auths.go
@@ -239,6 +239,7 @@ func NewAuthSourcePost(ctx *context.Context) {
 	case models.LoginPAM:
 		config = &models.PAMConfig{
 			ServiceName: form.PAMServiceName,
+			EmailDomain: form.PAMEmailDomain,
 		}
 	case models.LoginOAuth2:
 		config = parseOAuth2Config(form)
@@ -346,6 +347,7 @@ func EditAuthSourcePost(ctx *context.Context) {
 	case models.LoginPAM:
 		config = &models.PAMConfig{
 			ServiceName: form.PAMServiceName,
+			EmailDomain: form.PAMEmailDomain,
 		}
 	case models.LoginOAuth2:
 		config = parseOAuth2Config(form)
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index 6b24858395..96da7d2ed6 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -188,6 +188,10 @@
 						<label for="pam_service_name">{{.i18n.Tr "admin.auths.pam_service_name"}}</label>
 						<input id="pam_service_name" name="pam_service_name" value="{{$cfg.ServiceName}}" required>
 					</div>
+					<div class="field">
+						<label for="pam_email_domain">{{.i18n.Tr "admin.auths.pam_email_domain"}}</label>
+						<input id="pam_email_domain" name="pam_email_domain" value="{{$cfg.EmailDomain}}">
+					</div>
 				{{end}}
 
 				<!-- OAuth2 -->
diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl
index c252f7d3f2..7742e131dc 100644
--- a/templates/admin/auth/new.tmpl
+++ b/templates/admin/auth/new.tmpl
@@ -38,6 +38,8 @@
 				<div class="pam required field {{if not (eq .type 4)}}hide{{end}}">
 					<label for="pam_service_name">{{.i18n.Tr "admin.auths.pam_service_name"}}</label>
 					<input id="pam_service_name" name="pam_service_name" value="{{.pam_service_name}}" />
+					<label for="pam_email_domain">{{.i18n.Tr "admin.auths.pam_email_domain"}}</label>
+					<input id="pam_email_domain" name="pam_email_domain" value="{{.pam_email_domain}}">
 				</div>
 
 				<!-- OAuth2 -->