From 17b2079f3e06b95ab1ad752ed1218795538a1606 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Zoran=20Peri=C4=8Di=C4=87?= <zpericic@netst.org>
Date: Wed, 12 Jan 2022 23:54:53 +0100
Subject: [PATCH] Add/update SMTP auth providers via cli (#18197)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Allow adding/updating SMTP authentication source via CLI using:
- gitea admin  auth add-smtp
- gitea admin  auth update-smtp

Signed-off-by: Zoran Peričić <zpericic@netst.org>
---
 cmd/admin.go                                 | 181 +++++++++++++++++++
 docs/content/doc/usage/command-line.en-us.md |  27 +++
 2 files changed, 208 insertions(+)

diff --git a/cmd/admin.go b/cmd/admin.go
index ed2aa5de23..3c7f7c8a7c 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -27,6 +27,7 @@ import (
 	"code.gitea.io/gitea/modules/storage"
 	auth_service "code.gitea.io/gitea/services/auth"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
+	"code.gitea.io/gitea/services/auth/source/smtp"
 	repo_service "code.gitea.io/gitea/services/repository"
 	user_service "code.gitea.io/gitea/services/user"
 
@@ -190,6 +191,8 @@ var (
 			cmdAuthUpdateLdapBindDn,
 			cmdAuthAddLdapSimpleAuth,
 			cmdAuthUpdateLdapSimpleAuth,
+			microcmdAuthAddSMTP,
+			microcmdAuthUpdateSMTP,
 			microcmdAuthList,
 			microcmdAuthDelete,
 		},
@@ -366,6 +369,72 @@ var (
 			},
 		},
 	}
+
+	smtpCLIFlags = []cli.Flag{
+		cli.StringFlag{
+			Name:  "name",
+			Value: "",
+			Usage: "Application Name",
+		},
+		cli.StringFlag{
+			Name:  "auth-type",
+			Value: "PLAIN",
+			Usage: "SMTP Authentication Type (PLAIN/LOGIN/CRAM-MD5) default PLAIN",
+		},
+		cli.StringFlag{
+			Name:  "host",
+			Value: "",
+			Usage: "SMTP Host",
+		},
+		cli.IntFlag{
+			Name:  "port",
+			Usage: "SMTP Port",
+		},
+		cli.BoolTFlag{
+			Name:  "force-smtps",
+			Usage: "SMTPS is always used on port 465. Set this to force SMTPS on other ports.",
+		},
+		cli.BoolTFlag{
+			Name:  "skip-verify",
+			Usage: "Skip TLS verify.",
+		},
+		cli.StringFlag{
+			Name:  "helo-hostname",
+			Value: "",
+			Usage: "Hostname sent with HELO. Leave blank to send current hostname",
+		},
+		cli.BoolTFlag{
+			Name:  "disable-helo",
+			Usage: "Disable SMTP helo.",
+		},
+		cli.StringFlag{
+			Name:  "allowed-domains",
+			Value: "",
+			Usage: "Leave empty to allow all domains. Separate multiple domains with a comma (',')",
+		},
+		cli.BoolTFlag{
+			Name:  "skip-local-2fa",
+			Usage: "Skip 2FA to log on.",
+		},
+		cli.BoolTFlag{
+			Name:  "active",
+			Usage: "This Authentication Source is Activated.",
+		},
+	}
+
+	microcmdAuthAddSMTP = cli.Command{
+		Name:   "add-smtp",
+		Usage:  "Add new SMTP authentication source",
+		Action: runAddSMTP,
+		Flags:  smtpCLIFlags,
+	}
+
+	microcmdAuthUpdateSMTP = cli.Command{
+		Name:   "update-smtp",
+		Usage:  "Update existing SMTP authentication source",
+		Action: runUpdateSMTP,
+		Flags:  append(smtpCLIFlags[:1], append([]cli.Flag{idFlag}, smtpCLIFlags[1:]...)...),
+	}
 )
 
 func runChangePassword(c *cli.Context) error {
@@ -804,6 +873,118 @@ func runUpdateOauth(c *cli.Context) error {
 	return auth.UpdateSource(source)
 }
 
+func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error {
+	if c.IsSet("auth-type") {
+		conf.Auth = c.String("auth-type")
+		validAuthTypes := []string{"PLAIN", "LOGIN", "CRAM-MD5"}
+		if !contains(validAuthTypes, strings.ToUpper(c.String("auth-type"))) {
+			return errors.New("Auth must be one of PLAIN/LOGIN/CRAM-MD5")
+		}
+		conf.Auth = c.String("auth-type")
+	}
+	if c.IsSet("host") {
+		conf.Host = c.String("host")
+	}
+	if c.IsSet("port") {
+		conf.Port = c.Int("port")
+	}
+	if c.IsSet("allowed-domains") {
+		conf.AllowedDomains = c.String("allowed-domains")
+	}
+	if c.IsSet("force-smtps") {
+		conf.ForceSMTPS = c.BoolT("force-smtps")
+	}
+	if c.IsSet("skip-verify") {
+		conf.SkipVerify = c.BoolT("skip-verify")
+	}
+	if c.IsSet("helo-hostname") {
+		conf.HeloHostname = c.String("helo-hostname")
+	}
+	if c.IsSet("disable-helo") {
+		conf.DisableHelo = c.BoolT("disable-helo")
+	}
+	if c.IsSet("skip-local-2fa") {
+		conf.SkipLocalTwoFA = c.BoolT("skip-local-2fa")
+	}
+	return nil
+}
+
+func runAddSMTP(c *cli.Context) error {
+	ctx, cancel := installSignals()
+	defer cancel()
+
+	if err := initDB(ctx); err != nil {
+		return err
+	}
+
+	if !c.IsSet("name") || len(c.String("name")) == 0 {
+		return errors.New("name must be set")
+	}
+	if !c.IsSet("host") || len(c.String("host")) == 0 {
+		return errors.New("host must be set")
+	}
+	if !c.IsSet("port") {
+		return errors.New("port must be set")
+	}
+	var active = true
+	if c.IsSet("active") {
+		active = c.BoolT("active")
+	}
+
+	var smtpConfig smtp.Source
+	if err := parseSMTPConfig(c, &smtpConfig); err != nil {
+		return err
+	}
+
+	// If not set default to PLAIN
+	if len(smtpConfig.Auth) == 0 {
+		smtpConfig.Auth = "PLAIN"
+	}
+
+	return auth.CreateSource(&auth.Source{
+		Type:     auth.SMTP,
+		Name:     c.String("name"),
+		IsActive: active,
+		Cfg:      &smtpConfig,
+	})
+}
+
+func runUpdateSMTP(c *cli.Context) error {
+	if !c.IsSet("id") {
+		return fmt.Errorf("--id flag is missing")
+	}
+
+	ctx, cancel := installSignals()
+	defer cancel()
+
+	if err := initDB(ctx); err != nil {
+		return err
+	}
+
+	source, err := auth.GetSourceByID(c.Int64("id"))
+	if err != nil {
+		return err
+	}
+
+	smtpConfig := source.Cfg.(*smtp.Source)
+
+	if err := parseSMTPConfig(c, smtpConfig); err != nil {
+		return err
+	}
+
+	if c.IsSet("name") {
+		source.Name = c.String("name")
+	}
+
+	if c.IsSet("active") {
+		source.IsActive = c.BoolT("active")
+	}
+
+	source.Cfg = smtpConfig
+
+	return auth.UpdateSource(source)
+}
+
 func runListAuth(c *cli.Context) error {
 	ctx, cancel := installSignals()
 	defer cancel()
diff --git a/docs/content/doc/usage/command-line.en-us.md b/docs/content/doc/usage/command-line.en-us.md
index e477f31f1a..90e872e635 100644
--- a/docs/content/doc/usage/command-line.en-us.md
+++ b/docs/content/doc/usage/command-line.en-us.md
@@ -161,6 +161,33 @@ Admin operations:
         - `--restricted-group`: Group Claim value for restricted users. (Optional)
       - Examples:
         - `gitea admin auth update-oauth --id 1 --name external-github-updated`
+    - `add-smtp`:
+      - Options:
+        - `--name`: Application Name. Required.
+        - `--auth-type`: SMTP Authentication Type (PLAIN/LOGIN/CRAM-MD5). Default to PLAIN.
+        - `--host`: SMTP host. Required.
+        - `--port`: SMTP port. Required.
+        - `--force-smtps`: SMTPS is always used on port 465. Set this to force SMTPS on other ports.
+        - `--skip-verify`: Skip TLS verify.
+        - `--helo-hostname`: Hostname sent with HELO. Leave blank to send current hostname.
+        - `--disable-helo`: Disable SMTP helo.
+        - `--allowed-domains`: Leave empty to allow all domains. Separate multiple domains with a comma (',').
+        - `--skip-local-2fa`: Skip 2FA to log on.
+        - `--active`: This Authentication Source is Activated.
+        Remarks:
+        `--force-smtps`, `--skip-verify`, `--disable-helo`, `--skip-loca-2fs` and `--active` options can be used in form:
+        - `--option`, `--option=true` to enable
+        - `--option=false` to disable
+        If those options are not specified value would not be changed in `update-smtp` or would use default `false` value in `add-smtp`
+      - Examples:
+        - `gitea admin auth add-smtp --name ldap --host smtp.mydomain.org --port 587 --skip-verify --active`
+    - `update-smtp`:
+      - Options:
+        - `--id`: ID of source to be updated. Required.
+        - other options are shared with `add-smtp`
+      - Examples:
+        - `gitea admin auth update-smtp --id 1 --host smtp.mydomain.org --port 587 --skip-verify=false`
+        - `gitea admin auth update-smtp --id 1 --active=false`
     - `add-ldap`: Add new LDAP (via Bind DN) authentication source
       - Options:
         - `--name value`: Authentication name. Required.