From a618df8d8474efb9148d29c4ce6cf8ec14cfca7a Mon Sep 17 00:00:00 2001
From: ngourdon <ngourdon@gmail.com>
Date: Mon, 17 Jun 2019 20:32:20 +0200
Subject: [PATCH] Add CLI commands to manage LDAP authentication source (#6681)

* add CLI commands to manage LDAP authentication source

* delete Gogs copyright

* remove unused return value of func parseLoginSource

* fix comment

Co-Authored-By: ngourdon <31291059+ngourdon@users.noreply.github.com>

* remove config flag already present in global flags

* remove config flag from ldap commands in docs

* remove config flag handling
---
 cmd/admin.go                                 |    6 +-
 cmd/admin_auth_ldap.go                       |  359 +++++
 cmd/admin_auth_ldap_test.go                  | 1350 ++++++++++++++++++
 docs/content/doc/usage/command-line.en-us.md |   88 ++
 4 files changed, 1802 insertions(+), 1 deletion(-)
 create mode 100644 cmd/admin_auth_ldap.go
 create mode 100644 cmd/admin_auth_ldap_test.go

diff --git a/cmd/admin.go b/cmd/admin.go
index 6234ab828..4c4d6f9b6 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -131,6 +131,10 @@ var (
 		Subcommands: []cli.Command{
 			microcmdAuthAddOauth,
 			microcmdAuthUpdateOauth,
+			cmdAuthAddLdapBindDn,
+			cmdAuthUpdateLdapBindDn,
+			cmdAuthAddLdapSimpleAuth,
+			cmdAuthUpdateLdapSimpleAuth,
 			microcmdAuthList,
 			microcmdAuthDelete,
 		},
@@ -144,7 +148,7 @@ var (
 
 	idFlag = cli.Int64Flag{
 		Name:  "id",
-		Usage: "ID of OAuth authentication source",
+		Usage: "ID of authentication source",
 	}
 
 	microcmdAuthDelete = cli.Command{
diff --git a/cmd/admin_auth_ldap.go b/cmd/admin_auth_ldap.go
new file mode 100644
index 000000000..cce3aa894
--- /dev/null
+++ b/cmd/admin_auth_ldap.go
@@ -0,0 +1,359 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package cmd
+
+import (
+	"fmt"
+	"strings"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/auth/ldap"
+
+	"github.com/urfave/cli"
+)
+
+type (
+	authService struct {
+		initDB             func() error
+		createLoginSource  func(loginSource *models.LoginSource) error
+		updateLoginSource  func(loginSource *models.LoginSource) error
+		getLoginSourceByID func(id int64) (*models.LoginSource, error)
+	}
+)
+
+var (
+	commonLdapCLIFlags = []cli.Flag{
+		cli.StringFlag{
+			Name:  "name",
+			Usage: "Authentication name.",
+		},
+		cli.BoolFlag{
+			Name:  "not-active",
+			Usage: "Deactivate the authentication source.",
+		},
+		cli.StringFlag{
+			Name:  "security-protocol",
+			Usage: "Security protocol name.",
+		},
+		cli.BoolFlag{
+			Name:  "skip-tls-verify",
+			Usage: "Disable TLS verification.",
+		},
+		cli.StringFlag{
+			Name:  "host",
+			Usage: "The address where the LDAP server can be reached.",
+		},
+		cli.IntFlag{
+			Name:  "port",
+			Usage: "The port to use when connecting to the LDAP server.",
+		},
+		cli.StringFlag{
+			Name:  "user-search-base",
+			Usage: "The LDAP base at which user accounts will be searched for.",
+		},
+		cli.StringFlag{
+			Name:  "user-filter",
+			Usage: "An LDAP filter declaring how to find the user record that is attempting to authenticate.",
+		},
+		cli.StringFlag{
+			Name:  "admin-filter",
+			Usage: "An LDAP filter specifying if a user should be given administrator privileges.",
+		},
+		cli.StringFlag{
+			Name:  "username-attribute",
+			Usage: "The attribute of the user’s LDAP record containing the user name.",
+		},
+		cli.StringFlag{
+			Name:  "firstname-attribute",
+			Usage: "The attribute of the user’s LDAP record containing the user’s first name.",
+		},
+		cli.StringFlag{
+			Name:  "surname-attribute",
+			Usage: "The attribute of the user’s LDAP record containing the user’s surname.",
+		},
+		cli.StringFlag{
+			Name:  "email-attribute",
+			Usage: "The attribute of the user’s LDAP record containing the user’s email address.",
+		},
+		cli.StringFlag{
+			Name:  "public-ssh-key-attribute",
+			Usage: "The attribute of the user’s LDAP record containing the user’s public ssh key.",
+		},
+	}
+
+	ldapBindDnCLIFlags = append(commonLdapCLIFlags,
+		cli.StringFlag{
+			Name:  "bind-dn",
+			Usage: "The DN to bind to the LDAP server with when searching for the user.",
+		},
+		cli.StringFlag{
+			Name:  "bind-password",
+			Usage: "The password for the Bind DN, if any.",
+		},
+		cli.BoolFlag{
+			Name:  "attributes-in-bind",
+			Usage: "Fetch attributes in bind DN context.",
+		},
+		cli.BoolFlag{
+			Name:  "synchronize-users",
+			Usage: "Enable user synchronization.",
+		},
+		cli.UintFlag{
+			Name:  "page-size",
+			Usage: "Search page size.",
+		})
+
+	ldapSimpleAuthCLIFlags = append(commonLdapCLIFlags,
+		cli.StringFlag{
+			Name:  "user-dn",
+			Usage: "The user’s DN.",
+		})
+
+	cmdAuthAddLdapBindDn = cli.Command{
+		Name:  "add-ldap",
+		Usage: "Add new LDAP (via Bind DN) authentication source",
+		Action: func(c *cli.Context) error {
+			return newAuthService().addLdapBindDn(c)
+		},
+		Flags: ldapBindDnCLIFlags,
+	}
+
+	cmdAuthUpdateLdapBindDn = cli.Command{
+		Name:  "update-ldap",
+		Usage: "Update existing LDAP (via Bind DN) authentication source",
+		Action: func(c *cli.Context) error {
+			return newAuthService().updateLdapBindDn(c)
+		},
+		Flags: append([]cli.Flag{idFlag}, ldapBindDnCLIFlags...),
+	}
+
+	cmdAuthAddLdapSimpleAuth = cli.Command{
+		Name:  "add-ldap-simple",
+		Usage: "Add new LDAP (simple auth) authentication source",
+		Action: func(c *cli.Context) error {
+			return newAuthService().addLdapSimpleAuth(c)
+		},
+		Flags: ldapSimpleAuthCLIFlags,
+	}
+
+	cmdAuthUpdateLdapSimpleAuth = cli.Command{
+		Name:  "update-ldap-simple",
+		Usage: "Update existing LDAP (simple auth) authentication source",
+		Action: func(c *cli.Context) error {
+			return newAuthService().updateLdapSimpleAuth(c)
+		},
+		Flags: append([]cli.Flag{idFlag}, ldapSimpleAuthCLIFlags...),
+	}
+)
+
+// newAuthService creates a service with default functions.
+func newAuthService() *authService {
+	return &authService{
+		initDB:             initDB,
+		createLoginSource:  models.CreateLoginSource,
+		updateLoginSource:  models.UpdateSource,
+		getLoginSourceByID: models.GetLoginSourceByID,
+	}
+}
+
+// parseLoginSource assigns values on loginSource according to command line flags.
+func parseLoginSource(c *cli.Context, loginSource *models.LoginSource) {
+	if c.IsSet("name") {
+		loginSource.Name = c.String("name")
+	}
+	if c.IsSet("not-active") {
+		loginSource.IsActived = !c.Bool("not-active")
+	}
+	if c.IsSet("synchronize-users") {
+		loginSource.IsSyncEnabled = c.Bool("synchronize-users")
+	}
+}
+
+// parseLdapConfig assigns values on config according to command line flags.
+func parseLdapConfig(c *cli.Context, config *models.LDAPConfig) error {
+	if c.IsSet("name") {
+		config.Source.Name = c.String("name")
+	}
+	if c.IsSet("host") {
+		config.Source.Host = c.String("host")
+	}
+	if c.IsSet("port") {
+		config.Source.Port = c.Int("port")
+	}
+	if c.IsSet("security-protocol") {
+		p, ok := findLdapSecurityProtocolByName(c.String("security-protocol"))
+		if !ok {
+			return fmt.Errorf("Unknown security protocol name: %s", c.String("security-protocol"))
+		}
+		config.Source.SecurityProtocol = p
+	}
+	if c.IsSet("skip-tls-verify") {
+		config.Source.SkipVerify = c.Bool("skip-tls-verify")
+	}
+	if c.IsSet("bind-dn") {
+		config.Source.BindDN = c.String("bind-dn")
+	}
+	if c.IsSet("user-dn") {
+		config.Source.UserDN = c.String("user-dn")
+	}
+	if c.IsSet("bind-password") {
+		config.Source.BindPassword = c.String("bind-password")
+	}
+	if c.IsSet("user-search-base") {
+		config.Source.UserBase = c.String("user-search-base")
+	}
+	if c.IsSet("username-attribute") {
+		config.Source.AttributeUsername = c.String("username-attribute")
+	}
+	if c.IsSet("firstname-attribute") {
+		config.Source.AttributeName = c.String("firstname-attribute")
+	}
+	if c.IsSet("surname-attribute") {
+		config.Source.AttributeSurname = c.String("surname-attribute")
+	}
+	if c.IsSet("email-attribute") {
+		config.Source.AttributeMail = c.String("email-attribute")
+	}
+	if c.IsSet("attributes-in-bind") {
+		config.Source.AttributesInBind = c.Bool("attributes-in-bind")
+	}
+	if c.IsSet("public-ssh-key-attribute") {
+		config.Source.AttributeSSHPublicKey = c.String("public-ssh-key-attribute")
+	}
+	if c.IsSet("page-size") {
+		config.Source.SearchPageSize = uint32(c.Uint("page-size"))
+	}
+	if c.IsSet("user-filter") {
+		config.Source.Filter = c.String("user-filter")
+	}
+	if c.IsSet("admin-filter") {
+		config.Source.AdminFilter = c.String("admin-filter")
+	}
+	return nil
+}
+
+// findLdapSecurityProtocolByName finds security protocol by its name ignoring case.
+// It returns the value of the security protocol and if it was found.
+func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) {
+	for i, n := range models.SecurityProtocolNames {
+		if strings.EqualFold(name, n) {
+			return i, true
+		}
+	}
+	return 0, false
+}
+
+// getLoginSource gets the login source by its id defined in the command line flags.
+// It returns an error if the id is not set, does not match any source or if the source is not of expected type.
+func (a *authService) getLoginSource(c *cli.Context, loginType models.LoginType) (*models.LoginSource, error) {
+	if err := argsSet(c, "id"); err != nil {
+		return nil, err
+	}
+
+	loginSource, err := a.getLoginSourceByID(c.Int64("id"))
+	if err != nil {
+		return nil, err
+	}
+
+	if loginSource.Type != loginType {
+		return nil, fmt.Errorf("Invalid authentication type. expected: %s, actual: %s", models.LoginNames[loginType], models.LoginNames[loginSource.Type])
+	}
+
+	return loginSource, nil
+}
+
+// addLdapBindDn adds a new LDAP via Bind DN authentication source.
+func (a *authService) addLdapBindDn(c *cli.Context) error {
+	if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil {
+		return err
+	}
+
+	if err := a.initDB(); err != nil {
+		return err
+	}
+
+	loginSource := &models.LoginSource{
+		Type:      models.LoginLDAP,
+		IsActived: true, // active by default
+		Cfg: &models.LDAPConfig{
+			Source: &ldap.Source{
+				Enabled: true, // always true
+			},
+		},
+	}
+
+	parseLoginSource(c, loginSource)
+	if err := parseLdapConfig(c, loginSource.LDAP()); err != nil {
+		return err
+	}
+
+	return a.createLoginSource(loginSource)
+}
+
+// updateLdapBindDn updates a new LDAP via Bind DN authentication source.
+func (a *authService) updateLdapBindDn(c *cli.Context) error {
+	if err := a.initDB(); err != nil {
+		return err
+	}
+
+	loginSource, err := a.getLoginSource(c, models.LoginLDAP)
+	if err != nil {
+		return err
+	}
+
+	parseLoginSource(c, loginSource)
+	if err := parseLdapConfig(c, loginSource.LDAP()); err != nil {
+		return err
+	}
+
+	return a.updateLoginSource(loginSource)
+}
+
+// addLdapSimpleAuth adds a new LDAP (simple auth) authentication source.
+func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
+	if err := argsSet(c, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute"); err != nil {
+		return err
+	}
+
+	if err := a.initDB(); err != nil {
+		return err
+	}
+
+	loginSource := &models.LoginSource{
+		Type:      models.LoginDLDAP,
+		IsActived: true, // active by default
+		Cfg: &models.LDAPConfig{
+			Source: &ldap.Source{
+				Enabled: true, // always true
+			},
+		},
+	}
+
+	parseLoginSource(c, loginSource)
+	if err := parseLdapConfig(c, loginSource.LDAP()); err != nil {
+		return err
+	}
+
+	return a.createLoginSource(loginSource)
+}
+
+// updateLdapBindDn updates a new LDAP (simple auth) authentication source.
+func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
+	if err := a.initDB(); err != nil {
+		return err
+	}
+
+	loginSource, err := a.getLoginSource(c, models.LoginDLDAP)
+	if err != nil {
+		return err
+	}
+
+	parseLoginSource(c, loginSource)
+	if err := parseLdapConfig(c, loginSource.LDAP()); err != nil {
+		return err
+	}
+
+	return a.updateLoginSource(loginSource)
+}
diff --git a/cmd/admin_auth_ldap_test.go b/cmd/admin_auth_ldap_test.go
new file mode 100644
index 000000000..4af9f167c
--- /dev/null
+++ b/cmd/admin_auth_ldap_test.go
@@ -0,0 +1,1350 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package cmd
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/auth/ldap"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/urfave/cli"
+)
+
+func TestAddLdapBindDn(t *testing.T) {
+	// Mock cli functions to do not exit on error
+	var osExiter = cli.OsExiter
+	defer func() { cli.OsExiter = osExiter }()
+	cli.OsExiter = func(code int) {}
+
+	// Test cases
+	var cases = []struct {
+		args        []string
+		loginSource *models.LoginSource
+		errMsg      string
+	}{
+		// case 0
+		{
+			args: []string{
+				"ldap-test",
+				"--name", "ldap (via Bind DN) source full",
+				"--not-active",
+				"--security-protocol", "ldaps",
+				"--skip-tls-verify",
+				"--host", "ldap-bind-server full",
+				"--port", "9876",
+				"--user-search-base", "ou=Users,dc=full-domain-bind,dc=org",
+				"--user-filter", "(memberOf=cn=user-group,ou=example,dc=full-domain-bind,dc=org)",
+				"--admin-filter", "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)",
+				"--username-attribute", "uid-bind full",
+				"--firstname-attribute", "givenName-bind full",
+				"--surname-attribute", "sn-bind full",
+				"--email-attribute", "mail-bind full",
+				"--public-ssh-key-attribute", "publickey-bind full",
+				"--bind-dn", "cn=readonly,dc=full-domain-bind,dc=org",
+				"--bind-password", "secret-bind-full",
+				"--attributes-in-bind",
+				"--synchronize-users",
+				"--page-size", "99",
+			},
+			loginSource: &models.LoginSource{
+				Type:          models.LoginLDAP,
+				Name:          "ldap (via Bind DN) source full",
+				IsActived:     false,
+				IsSyncEnabled: true,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						Name:                  "ldap (via Bind DN) source full",
+						Host:                  "ldap-bind-server full",
+						Port:                  9876,
+						SecurityProtocol:      ldap.SecurityProtocol(1),
+						SkipVerify:            true,
+						BindDN:                "cn=readonly,dc=full-domain-bind,dc=org",
+						BindPassword:          "secret-bind-full",
+						UserBase:              "ou=Users,dc=full-domain-bind,dc=org",
+						AttributeUsername:     "uid-bind full",
+						AttributeName:         "givenName-bind full",
+						AttributeSurname:      "sn-bind full",
+						AttributeMail:         "mail-bind full",
+						AttributesInBind:      true,
+						AttributeSSHPublicKey: "publickey-bind full",
+						SearchPageSize:        99,
+						Filter:                "(memberOf=cn=user-group,ou=example,dc=full-domain-bind,dc=org)",
+						AdminFilter:           "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)",
+						Enabled:               true,
+					},
+				},
+			},
+		},
+		// case 1
+		{
+			args: []string{
+				"ldap-test",
+				"--name", "ldap (via Bind DN) source min",
+				"--security-protocol", "unencrypted",
+				"--host", "ldap-bind-server min",
+				"--port", "1234",
+				"--user-search-base", "ou=Users,dc=min-domain-bind,dc=org",
+				"--user-filter", "(memberOf=cn=user-group,ou=example,dc=min-domain-bind,dc=org)",
+				"--email-attribute", "mail-bind min",
+			},
+			loginSource: &models.LoginSource{
+				Type:      models.LoginLDAP,
+				Name:      "ldap (via Bind DN) source min",
+				IsActived: true,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						Name:             "ldap (via Bind DN) source min",
+						Host:             "ldap-bind-server min",
+						Port:             1234,
+						SecurityProtocol: ldap.SecurityProtocol(0),
+						UserBase:         "ou=Users,dc=min-domain-bind,dc=org",
+						AttributeMail:    "mail-bind min",
+						Filter:           "(memberOf=cn=user-group,ou=example,dc=min-domain-bind,dc=org)",
+						Enabled:          true,
+					},
+				},
+			},
+		},
+		// case 2
+		{
+			args: []string{
+				"ldap-test",
+				"--name", "ldap (via Bind DN) source",
+				"--security-protocol", "zzzzz",
+				"--host", "ldap-server",
+				"--port", "1234",
+				"--user-search-base", "ou=Users,dc=domain,dc=org",
+				"--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
+				"--email-attribute", "mail",
+			},
+			errMsg: "Unknown security protocol name: zzzzz",
+		},
+		// case 3
+		{
+			args: []string{
+				"ldap-test",
+				"--security-protocol", "unencrypted",
+				"--host", "ldap-server",
+				"--port", "1234",
+				"--user-search-base", "ou=Users,dc=domain,dc=org",
+				"--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
+				"--email-attribute", "mail",
+			},
+			errMsg: "name is not set",
+		},
+		// case 4
+		{
+			args: []string{
+				"ldap-test",
+				"--name", "ldap (via Bind DN) source",
+				"--host", "ldap-server",
+				"--port", "1234",
+				"--user-search-base", "ou=Users,dc=domain,dc=org",
+				"--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
+				"--email-attribute", "mail",
+			},
+			errMsg: "security-protocol is not set",
+		},
+		// case 5
+		{
+			args: []string{
+				"ldap-test",
+				"--name", "ldap (via Bind DN) source",
+				"--security-protocol", "unencrypted",
+				"--port", "1234",
+				"--user-search-base", "ou=Users,dc=domain,dc=org",
+				"--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
+				"--email-attribute", "mail",
+			},
+			errMsg: "host is not set",
+		},
+		// case 6
+		{
+			args: []string{
+				"ldap-test",
+				"--name", "ldap (via Bind DN) source",
+				"--security-protocol", "unencrypted",
+				"--host", "ldap-server",
+				"--user-search-base", "ou=Users,dc=domain,dc=org",
+				"--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
+				"--email-attribute", "mail",
+			},
+			errMsg: "port is not set",
+		},
+		// case 7
+		{
+			args: []string{
+				"ldap-test",
+				"--name", "ldap (via Bind DN) source",
+				"--security-protocol", "unencrypted",
+				"--host", "ldap-server",
+				"--port", "1234",
+				"--user-search-base", "ou=Users,dc=domain,dc=org",
+				"--email-attribute", "mail",
+			},
+			errMsg: "user-filter is not set",
+		},
+		// case 8
+		{
+			args: []string{
+				"ldap-test",
+				"--name", "ldap (via Bind DN) source",
+				"--security-protocol", "unencrypted",
+				"--host", "ldap-server",
+				"--port", "1234",
+				"--user-search-base", "ou=Users,dc=domain,dc=org",
+				"--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
+			},
+			errMsg: "email-attribute is not set",
+		},
+	}
+
+	for n, c := range cases {
+		// Mock functions.
+		var createdLoginSource *models.LoginSource
+		service := &authService{
+			initDB: func() error {
+				return nil
+			},
+			createLoginSource: func(loginSource *models.LoginSource) error {
+				createdLoginSource = loginSource
+				return nil
+			},
+			updateLoginSource: func(loginSource *models.LoginSource) error {
+				assert.FailNow(t, "case %d: should not call updateLoginSource", n)
+				return nil
+			},
+			getLoginSourceByID: func(id int64) (*models.LoginSource, error) {
+				assert.FailNow(t, "case %d: should not call getLoginSourceByID", n)
+				return nil, nil
+			},
+		}
+
+		// Create a copy of command to test
+		app := cli.NewApp()
+		app.Flags = cmdAuthAddLdapBindDn.Flags
+		app.Action = service.addLdapBindDn
+
+		// Run it
+		err := app.Run(c.args)
+		if c.errMsg != "" {
+			assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
+		} else {
+			assert.NoError(t, err, "case %d: should have no errors", n)
+			assert.Equal(t, c.loginSource, createdLoginSource, "case %d: wrong loginSource", n)
+		}
+	}
+}
+
+func TestAddLdapSimpleAuth(t *testing.T) {
+	// Mock cli functions to do not exit on error
+	var osExiter = cli.OsExiter
+	defer func() { cli.OsExiter = osExiter }()
+	cli.OsExiter = func(code int) {}
+
+	// Test cases
+	var cases = []struct {
+		args        []string
+		loginSource *models.LoginSource
+		errMsg      string
+	}{
+		// case 0
+		{
+			args: []string{
+				"ldap-test",
+				"--name", "ldap (simple auth) source full",
+				"--not-active",
+				"--security-protocol", "starttls",
+				"--skip-tls-verify",
+				"--host", "ldap-simple-server full",
+				"--port", "987",
+				"--user-search-base", "ou=Users,dc=full-domain-simple,dc=org",
+				"--user-filter", "(&(objectClass=posixAccount)(full-simple-cn=%s))",
+				"--admin-filter", "(memberOf=cn=admin-group,ou=example,dc=full-domain-simple,dc=org)",
+				"--username-attribute", "uid-simple full",
+				"--firstname-attribute", "givenName-simple full",
+				"--surname-attribute", "sn-simple full",
+				"--email-attribute", "mail-simple full",
+				"--public-ssh-key-attribute", "publickey-simple full",
+				"--user-dn", "cn=%s,ou=Users,dc=full-domain-simple,dc=org",
+			},
+			loginSource: &models.LoginSource{
+				Type:      models.LoginDLDAP,
+				Name:      "ldap (simple auth) source full",
+				IsActived: false,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						Name:                  "ldap (simple auth) source full",
+						Host:                  "ldap-simple-server full",
+						Port:                  987,
+						SecurityProtocol:      ldap.SecurityProtocol(2),
+						SkipVerify:            true,
+						UserDN:                "cn=%s,ou=Users,dc=full-domain-simple,dc=org",
+						UserBase:              "ou=Users,dc=full-domain-simple,dc=org",
+						AttributeUsername:     "uid-simple full",
+						AttributeName:         "givenName-simple full",
+						AttributeSurname:      "sn-simple full",
+						AttributeMail:         "mail-simple full",
+						AttributeSSHPublicKey: "publickey-simple full",
+						Filter:                "(&(objectClass=posixAccount)(full-simple-cn=%s))",
+						AdminFilter:           "(memberOf=cn=admin-group,ou=example,dc=full-domain-simple,dc=org)",
+						Enabled:               true,
+					},
+				},
+			},
+		},
+		// case 1
+		{
+			args: []string{
+				"ldap-test",
+				"--name", "ldap (simple auth) source min",
+				"--security-protocol", "unencrypted",
+				"--host", "ldap-simple-server min",
+				"--port", "123",
+				"--user-filter", "(&(objectClass=posixAccount)(min-simple-cn=%s))",
+				"--email-attribute", "mail-simple min",
+				"--user-dn", "cn=%s,ou=Users,dc=min-domain-simple,dc=org",
+			},
+			loginSource: &models.LoginSource{
+				Type:      models.LoginDLDAP,
+				Name:      "ldap (simple auth) source min",
+				IsActived: true,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						Name:             "ldap (simple auth) source min",
+						Host:             "ldap-simple-server min",
+						Port:             123,
+						SecurityProtocol: ldap.SecurityProtocol(0),
+						UserDN:           "cn=%s,ou=Users,dc=min-domain-simple,dc=org",
+						AttributeMail:    "mail-simple min",
+						Filter:           "(&(objectClass=posixAccount)(min-simple-cn=%s))",
+						Enabled:          true,
+					},
+				},
+			},
+		},
+		// case 2
+		{
+			args: []string{
+				"ldap-test",
+				"--name", "ldap (simple auth) source",
+				"--security-protocol", "zzzzz",
+				"--host", "ldap-server",
+				"--port", "123",
+				"--user-filter", "(&(objectClass=posixAccount)(cn=%s))",
+				"--email-attribute", "mail",
+				"--user-dn", "cn=%s,ou=Users,dc=domain,dc=org",
+			},
+			errMsg: "Unknown security protocol name: zzzzz",
+		},
+		// case 3
+		{
+			args: []string{
+				"ldap-test",
+				"--security-protocol", "unencrypted",
+				"--host", "ldap-server",
+				"--port", "123",
+				"--user-filter", "(&(objectClass=posixAccount)(cn=%s))",
+				"--email-attribute", "mail",
+				"--user-dn", "cn=%s,ou=Users,dc=domain,dc=org",
+			},
+			errMsg: "name is not set",
+		},
+		// case 4
+		{
+			args: []string{
+				"ldap-test",
+				"--name", "ldap (simple auth) source",
+				"--host", "ldap-server",
+				"--port", "123",
+				"--user-filter", "(&(objectClass=posixAccount)(cn=%s))",
+				"--email-attribute", "mail",
+				"--user-dn", "cn=%s,ou=Users,dc=domain,dc=org",
+			},
+			errMsg: "security-protocol is not set",
+		},
+		// case 5
+		{
+			args: []string{
+				"ldap-test",
+				"--name", "ldap (simple auth) source",
+				"--security-protocol", "unencrypted",
+				"--port", "123",
+				"--user-filter", "(&(objectClass=posixAccount)(cn=%s))",
+				"--email-attribute", "mail",
+				"--user-dn", "cn=%s,ou=Users,dc=domain,dc=org",
+			},
+			errMsg: "host is not set",
+		},
+		// case 6
+		{
+			args: []string{
+				"ldap-test",
+				"--name", "ldap (simple auth) source",
+				"--security-protocol", "unencrypted",
+				"--host", "ldap-server",
+				"--user-filter", "(&(objectClass=posixAccount)(cn=%s))",
+				"--email-attribute", "mail",
+				"--user-dn", "cn=%s,ou=Users,dc=domain,dc=org",
+			},
+			errMsg: "port is not set",
+		},
+		// case 7
+		{
+			args: []string{
+				"ldap-test",
+				"--name", "ldap (simple auth) source",
+				"--security-protocol", "unencrypted",
+				"--host", "ldap-server",
+				"--port", "123",
+				"--email-attribute", "mail",
+				"--user-dn", "cn=%s,ou=Users,dc=domain,dc=org",
+			},
+			errMsg: "user-filter is not set",
+		},
+		// case 8
+		{
+			args: []string{
+				"ldap-test",
+				"--name", "ldap (simple auth) source",
+				"--security-protocol", "unencrypted",
+				"--host", "ldap-server",
+				"--port", "123",
+				"--user-filter", "(&(objectClass=posixAccount)(cn=%s))",
+				"--user-dn", "cn=%s,ou=Users,dc=domain,dc=org",
+			},
+			errMsg: "email-attribute is not set",
+		},
+		// case 9
+		{
+			args: []string{
+				"ldap-test",
+				"--name", "ldap (simple auth) source",
+				"--security-protocol", "unencrypted",
+				"--host", "ldap-server",
+				"--port", "123",
+				"--user-filter", "(&(objectClass=posixAccount)(cn=%s))",
+				"--email-attribute", "mail",
+			},
+			errMsg: "user-dn is not set",
+		},
+	}
+
+	for n, c := range cases {
+		// Mock functions.
+		var createdLoginSource *models.LoginSource
+		service := &authService{
+			initDB: func() error {
+				return nil
+			},
+			createLoginSource: func(loginSource *models.LoginSource) error {
+				createdLoginSource = loginSource
+				return nil
+			},
+			updateLoginSource: func(loginSource *models.LoginSource) error {
+				assert.FailNow(t, "case %d: should not call updateLoginSource", n)
+				return nil
+			},
+			getLoginSourceByID: func(id int64) (*models.LoginSource, error) {
+				assert.FailNow(t, "case %d: should not call getLoginSourceByID", n)
+				return nil, nil
+			},
+		}
+
+		// Create a copy of command to test
+		app := cli.NewApp()
+		app.Flags = cmdAuthAddLdapSimpleAuth.Flags
+		app.Action = service.addLdapSimpleAuth
+
+		// Run it
+		err := app.Run(c.args)
+		if c.errMsg != "" {
+			assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
+		} else {
+			assert.NoError(t, err, "case %d: should have no errors", n)
+			assert.Equal(t, c.loginSource, createdLoginSource, "case %d: wrong loginSource", n)
+		}
+	}
+}
+
+func TestUpdateLdapBindDn(t *testing.T) {
+	// Mock cli functions to do not exit on error
+	var osExiter = cli.OsExiter
+	defer func() { cli.OsExiter = osExiter }()
+	cli.OsExiter = func(code int) {}
+
+	// Test cases
+	var cases = []struct {
+		args                []string
+		id                  int64
+		existingLoginSource *models.LoginSource
+		loginSource         *models.LoginSource
+		errMsg              string
+	}{
+		// case 0
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "23",
+				"--name", "ldap (via Bind DN) source full",
+				"--not-active",
+				"--security-protocol", "LDAPS",
+				"--skip-tls-verify",
+				"--host", "ldap-bind-server full",
+				"--port", "9876",
+				"--user-search-base", "ou=Users,dc=full-domain-bind,dc=org",
+				"--user-filter", "(memberOf=cn=user-group,ou=example,dc=full-domain-bind,dc=org)",
+				"--admin-filter", "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)",
+				"--username-attribute", "uid-bind full",
+				"--firstname-attribute", "givenName-bind full",
+				"--surname-attribute", "sn-bind full",
+				"--email-attribute", "mail-bind full",
+				"--public-ssh-key-attribute", "publickey-bind full",
+				"--bind-dn", "cn=readonly,dc=full-domain-bind,dc=org",
+				"--bind-password", "secret-bind-full",
+				"--synchronize-users",
+				"--page-size", "99",
+			},
+			id: 23,
+			existingLoginSource: &models.LoginSource{
+				Type:      models.LoginLDAP,
+				IsActived: true,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						Enabled: true,
+					},
+				},
+			},
+			loginSource: &models.LoginSource{
+				Type:          models.LoginLDAP,
+				Name:          "ldap (via Bind DN) source full",
+				IsActived:     false,
+				IsSyncEnabled: true,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						Name:                  "ldap (via Bind DN) source full",
+						Host:                  "ldap-bind-server full",
+						Port:                  9876,
+						SecurityProtocol:      ldap.SecurityProtocol(1),
+						SkipVerify:            true,
+						BindDN:                "cn=readonly,dc=full-domain-bind,dc=org",
+						BindPassword:          "secret-bind-full",
+						UserBase:              "ou=Users,dc=full-domain-bind,dc=org",
+						AttributeUsername:     "uid-bind full",
+						AttributeName:         "givenName-bind full",
+						AttributeSurname:      "sn-bind full",
+						AttributeMail:         "mail-bind full",
+						AttributesInBind:      false,
+						AttributeSSHPublicKey: "publickey-bind full",
+						SearchPageSize:        99,
+						Filter:                "(memberOf=cn=user-group,ou=example,dc=full-domain-bind,dc=org)",
+						AdminFilter:           "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)",
+						Enabled:               true,
+					},
+				},
+			},
+		},
+		// case 1
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{},
+				},
+			},
+		},
+		// case 2
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--name", "ldap (via Bind DN) source",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginLDAP,
+				Name: "ldap (via Bind DN) source",
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						Name: "ldap (via Bind DN) source",
+					},
+				},
+			},
+		},
+		// case 3
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--not-active",
+			},
+			existingLoginSource: &models.LoginSource{
+				Type:      models.LoginLDAP,
+				IsActived: true,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{},
+				},
+			},
+			loginSource: &models.LoginSource{
+				Type:      models.LoginLDAP,
+				IsActived: false,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{},
+				},
+			},
+		},
+		// case 4
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--security-protocol", "LDAPS",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						SecurityProtocol: ldap.SecurityProtocol(1),
+					},
+				},
+			},
+		},
+		// case 5
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--skip-tls-verify",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						SkipVerify: true,
+					},
+				},
+			},
+		},
+		// case 6
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--host", "ldap-server",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						Host: "ldap-server",
+					},
+				},
+			},
+		},
+		// case 7
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--port", "389",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						Port: 389,
+					},
+				},
+			},
+		},
+		// case 8
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--user-search-base", "ou=Users,dc=domain,dc=org",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						UserBase: "ou=Users,dc=domain,dc=org",
+					},
+				},
+			},
+		},
+		// case 9
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						Filter: "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
+					},
+				},
+			},
+		},
+		// case 10
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--admin-filter", "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
+					},
+				},
+			},
+		},
+		// case 11
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--username-attribute", "uid",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						AttributeUsername: "uid",
+					},
+				},
+			},
+		},
+		// case 12
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--firstname-attribute", "givenName",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						AttributeName: "givenName",
+					},
+				},
+			},
+		},
+		// case 13
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--surname-attribute", "sn",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						AttributeSurname: "sn",
+					},
+				},
+			},
+		},
+		// case 14
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--email-attribute", "mail",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						AttributeMail: "mail",
+					},
+				},
+			},
+		},
+		// case 15
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--attributes-in-bind",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						AttributesInBind: true,
+					},
+				},
+			},
+		},
+		// case 16
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--public-ssh-key-attribute", "publickey",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						AttributeSSHPublicKey: "publickey",
+					},
+				},
+			},
+		},
+		// case 17
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--bind-dn", "cn=readonly,dc=domain,dc=org",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						BindDN: "cn=readonly,dc=domain,dc=org",
+					},
+				},
+			},
+		},
+		// case 18
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--bind-password", "secret",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						BindPassword: "secret",
+					},
+				},
+			},
+		},
+		// case 19
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--synchronize-users",
+			},
+			loginSource: &models.LoginSource{
+				Type:          models.LoginLDAP,
+				IsSyncEnabled: true,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{},
+				},
+			},
+		},
+		// case 20
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--page-size", "12",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						SearchPageSize: 12,
+					},
+				},
+			},
+		},
+		// case 21
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--security-protocol", "xxxxx",
+			},
+			errMsg: "Unknown security protocol name: xxxxx",
+		},
+		// case 22
+		{
+			args: []string{
+				"ldap-test",
+			},
+			errMsg: "id is not set",
+		},
+		// case 23
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+			},
+			existingLoginSource: &models.LoginSource{
+				Type: models.LoginOAuth2,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{},
+				},
+			},
+			errMsg: "Invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2",
+		},
+	}
+
+	for n, c := range cases {
+		// Mock functions.
+		var updatedLoginSource *models.LoginSource
+		service := &authService{
+			initDB: func() error {
+				return nil
+			},
+			createLoginSource: func(loginSource *models.LoginSource) error {
+				assert.FailNow(t, "case %d: should not call createLoginSource", n)
+				return nil
+			},
+			updateLoginSource: func(loginSource *models.LoginSource) error {
+				updatedLoginSource = loginSource
+				return nil
+			},
+			getLoginSourceByID: func(id int64) (*models.LoginSource, error) {
+				if c.id != 0 {
+					assert.Equal(t, c.id, id, "case %d: wrong id", n)
+				}
+				if c.existingLoginSource != nil {
+					return c.existingLoginSource, nil
+				}
+				return &models.LoginSource{
+					Type: models.LoginLDAP,
+					Cfg: &models.LDAPConfig{
+						Source: &ldap.Source{},
+					},
+				}, nil
+			},
+		}
+
+		// Create a copy of command to test
+		app := cli.NewApp()
+		app.Flags = cmdAuthUpdateLdapBindDn.Flags
+		app.Action = service.updateLdapBindDn
+
+		// Run it
+		err := app.Run(c.args)
+		if c.errMsg != "" {
+			assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
+		} else {
+			assert.NoError(t, err, "case %d: should have no errors", n)
+			assert.Equal(t, c.loginSource, updatedLoginSource, "case %d: wrong loginSource", n)
+		}
+	}
+}
+
+func TestUpdateLdapSimpleAuth(t *testing.T) {
+	// Mock cli functions to do not exit on error
+	var osExiter = cli.OsExiter
+	defer func() { cli.OsExiter = osExiter }()
+	cli.OsExiter = func(code int) {}
+
+	// Test cases
+	var cases = []struct {
+		args                []string
+		id                  int64
+		existingLoginSource *models.LoginSource
+		loginSource         *models.LoginSource
+		errMsg              string
+	}{
+		// case 0
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "7",
+				"--name", "ldap (simple auth) source full",
+				"--not-active",
+				"--security-protocol", "starttls",
+				"--skip-tls-verify",
+				"--host", "ldap-simple-server full",
+				"--port", "987",
+				"--user-search-base", "ou=Users,dc=full-domain-simple,dc=org",
+				"--user-filter", "(&(objectClass=posixAccount)(full-simple-cn=%s))",
+				"--admin-filter", "(memberOf=cn=admin-group,ou=example,dc=full-domain-simple,dc=org)",
+				"--username-attribute", "uid-simple full",
+				"--firstname-attribute", "givenName-simple full",
+				"--surname-attribute", "sn-simple full",
+				"--email-attribute", "mail-simple full",
+				"--public-ssh-key-attribute", "publickey-simple full",
+				"--user-dn", "cn=%s,ou=Users,dc=full-domain-simple,dc=org",
+			},
+			id: 7,
+			loginSource: &models.LoginSource{
+				Type:      models.LoginDLDAP,
+				Name:      "ldap (simple auth) source full",
+				IsActived: false,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						Name:                  "ldap (simple auth) source full",
+						Host:                  "ldap-simple-server full",
+						Port:                  987,
+						SecurityProtocol:      ldap.SecurityProtocol(2),
+						SkipVerify:            true,
+						UserDN:                "cn=%s,ou=Users,dc=full-domain-simple,dc=org",
+						UserBase:              "ou=Users,dc=full-domain-simple,dc=org",
+						AttributeUsername:     "uid-simple full",
+						AttributeName:         "givenName-simple full",
+						AttributeSurname:      "sn-simple full",
+						AttributeMail:         "mail-simple full",
+						AttributeSSHPublicKey: "publickey-simple full",
+						Filter:                "(&(objectClass=posixAccount)(full-simple-cn=%s))",
+						AdminFilter:           "(memberOf=cn=admin-group,ou=example,dc=full-domain-simple,dc=org)",
+					},
+				},
+			},
+		},
+		// case 1
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginDLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{},
+				},
+			},
+		},
+		// case 2
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--name", "ldap (simple auth) source",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginDLDAP,
+				Name: "ldap (simple auth) source",
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						Name: "ldap (simple auth) source",
+					},
+				},
+			},
+		},
+		// case 3
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--not-active",
+			},
+			existingLoginSource: &models.LoginSource{
+				Type:      models.LoginDLDAP,
+				IsActived: true,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{},
+				},
+			},
+			loginSource: &models.LoginSource{
+				Type:      models.LoginDLDAP,
+				IsActived: false,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{},
+				},
+			},
+		},
+		// case 4
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--security-protocol", "starttls",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginDLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						SecurityProtocol: ldap.SecurityProtocol(2),
+					},
+				},
+			},
+		},
+		// case 5
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--skip-tls-verify",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginDLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						SkipVerify: true,
+					},
+				},
+			},
+		},
+		// case 6
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--host", "ldap-server",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginDLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						Host: "ldap-server",
+					},
+				},
+			},
+		},
+		// case 7
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--port", "987",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginDLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						Port: 987,
+					},
+				},
+			},
+		},
+		// case 8
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--user-search-base", "ou=Users,dc=domain,dc=org",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginDLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						UserBase: "ou=Users,dc=domain,dc=org",
+					},
+				},
+			},
+		},
+		// case 9
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--user-filter", "(&(objectClass=posixAccount)(cn=%s))",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginDLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						Filter: "(&(objectClass=posixAccount)(cn=%s))",
+					},
+				},
+			},
+		},
+		// case 10
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--admin-filter", "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginDLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
+					},
+				},
+			},
+		},
+		// case 11
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--username-attribute", "uid",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginDLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						AttributeUsername: "uid",
+					},
+				},
+			},
+		},
+		// case 12
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--firstname-attribute", "givenName",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginDLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						AttributeName: "givenName",
+					},
+				},
+			},
+		},
+		// case 13
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--surname-attribute", "sn",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginDLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						AttributeSurname: "sn",
+					},
+				},
+			},
+		},
+		// case 14
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--email-attribute", "mail",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginDLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						AttributeMail: "mail",
+					},
+				},
+			},
+		},
+		// case 15
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--public-ssh-key-attribute", "publickey",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginDLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						AttributeSSHPublicKey: "publickey",
+					},
+				},
+			},
+		},
+		// case 16
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--user-dn", "cn=%s,ou=Users,dc=domain,dc=org",
+			},
+			loginSource: &models.LoginSource{
+				Type: models.LoginDLDAP,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{
+						UserDN: "cn=%s,ou=Users,dc=domain,dc=org",
+					},
+				},
+			},
+		},
+		// case 17
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+				"--security-protocol", "xxxxx",
+			},
+			errMsg: "Unknown security protocol name: xxxxx",
+		},
+		// case 18
+		{
+			args: []string{
+				"ldap-test",
+			},
+			errMsg: "id is not set",
+		},
+		// case 19
+		{
+			args: []string{
+				"ldap-test",
+				"--id", "1",
+			},
+			existingLoginSource: &models.LoginSource{
+				Type: models.LoginPAM,
+				Cfg: &models.LDAPConfig{
+					Source: &ldap.Source{},
+				},
+			},
+			errMsg: "Invalid authentication type. expected: LDAP (simple auth), actual: PAM",
+		},
+	}
+
+	for n, c := range cases {
+		// Mock functions.
+		var updatedLoginSource *models.LoginSource
+		service := &authService{
+			initDB: func() error {
+				return nil
+			},
+			createLoginSource: func(loginSource *models.LoginSource) error {
+				assert.FailNow(t, "case %d: should not call createLoginSource", n)
+				return nil
+			},
+			updateLoginSource: func(loginSource *models.LoginSource) error {
+				updatedLoginSource = loginSource
+				return nil
+			},
+			getLoginSourceByID: func(id int64) (*models.LoginSource, error) {
+				if c.id != 0 {
+					assert.Equal(t, c.id, id, "case %d: wrong id", n)
+				}
+				if c.existingLoginSource != nil {
+					return c.existingLoginSource, nil
+				}
+				return &models.LoginSource{
+					Type: models.LoginDLDAP,
+					Cfg: &models.LDAPConfig{
+						Source: &ldap.Source{},
+					},
+				}, nil
+			},
+		}
+
+		// Create a copy of command to test
+		app := cli.NewApp()
+		app.Flags = cmdAuthUpdateLdapSimpleAuth.Flags
+		app.Action = service.updateLdapSimpleAuth
+
+		// Run it
+		err := app.Run(c.args)
+		if c.errMsg != "" {
+			assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
+		} else {
+			assert.NoError(t, err, "case %d: should have no errors", n)
+			assert.Equal(t, c.loginSource, updatedLoginSource, "case %d: wrong loginSource", n)
+		}
+	}
+}
diff --git a/docs/content/doc/usage/command-line.en-us.md b/docs/content/doc/usage/command-line.en-us.md
index 516e46ff0..0955680af 100644
--- a/docs/content/doc/usage/command-line.en-us.md
+++ b/docs/content/doc/usage/command-line.en-us.md
@@ -123,6 +123,94 @@ Admin operations:
                 - `--custom-email-url`: Use a custom Email URL (option for GitHub).
             - Examples:
                 - `gitea admin auth update-oauth --id 1 --name external-github-updated`
+        - `add-ldap`: Add new LDAP (via Bind DN) authentication source
+            - Options:
+                - `--name value`: Authentication name. Required.
+                - `--not-active`: Deactivate the authentication source.
+                - `--security-protocol value`: Security protocol name. Required.
+                - `--skip-tls-verify`: Disable TLS verification.
+                - `--host value`: The address where the LDAP server can be reached. Required.
+                - `--port value`: The port to use when connecting to the LDAP server. Required.
+                - `--user-search-base value`: The LDAP base at which user accounts will be searched for. Required.
+                - `--user-filter value`: An LDAP filter declaring how to find the user record that is attempting to authenticate. Required.
+                - `--admin-filter value`: An LDAP filter specifying if a user should be given administrator privileges.
+                - `--username-attribute value`: The attribute of the user’s LDAP record containing the user name.
+                - `--firstname-attribute value`: The attribute of the user’s LDAP record containing the user’s first name.
+                - `--surname-attribute value`: The attribute of the user’s LDAP record containing the user’s surname.
+                - `--email-attribute value`: The attribute of the user’s LDAP record containing the user’s email address. Required.
+                - `--public-ssh-key-attribute value`: The attribute of the user’s LDAP record containing the user’s public ssh key.
+                - `--bind-dn value`: The DN to bind to the LDAP server with when searching for the user.
+                - `--bind-password value`: The password for the Bind DN, if any.
+                - `--attributes-in-bind`: Fetch attributes in bind DN context.
+                - `--synchronize-users`: Enable user synchronization.
+                - `--page-size value`: Search page size.
+            - Examples:
+                - `gitea admin auth add-ldap --name ldap --security-protocol unencrypted --host mydomain.org --port 389 --user-search-base "ou=Users,dc=mydomain,dc=org" --user-filter "(&(objectClass=posixAccount)(uid=%s))" --email-attribute mail`
+        - `update-ldap`: Update existing LDAP (via Bind DN) authentication source
+            - Options:
+                - `--id value`: ID of authentication source. Required.
+                - `--name value`: Authentication name.
+                - `--not-active`: Deactivate the authentication source.
+                - `--security-protocol value`: Security protocol name.
+                - `--skip-tls-verify`: Disable TLS verification.
+                - `--host value`: The address where the LDAP server can be reached.
+                - `--port value`: The port to use when connecting to the LDAP server.
+                - `--user-search-base value`: The LDAP base at which user accounts will be searched for.
+                - `--user-filter value`: An LDAP filter declaring how to find the user record that is attempting to authenticate.
+                - `--admin-filter value`: An LDAP filter specifying if a user should be given administrator privileges.
+                - `--username-attribute value`: The attribute of the user’s LDAP record containing the user name.
+                - `--firstname-attribute value`: The attribute of the user’s LDAP record containing the user’s first name.
+                - `--surname-attribute value`: The attribute of the user’s LDAP record containing the user’s surname.
+                - `--email-attribute value`: The attribute of the user’s LDAP record containing the user’s email address.
+                - `--public-ssh-key-attribute value`: The attribute of the user’s LDAP record containing the user’s public ssh key.
+                - `--bind-dn value`: The DN to bind to the LDAP server with when searching for the user.
+                - `--bind-password value`: The password for the Bind DN, if any.
+                - `--attributes-in-bind`: Fetch attributes in bind DN context.
+                - `--synchronize-users`: Enable user synchronization.
+                - `--page-size value`: Search page size.
+            - Examples:
+                - `gitea admin auth update-ldap --id 1 --name "my ldap auth source"`
+                - `gitea admin auth update-ldap --id 1 --username-attribute uid --firstname-attribute givenName --surname-attribute sn`
+        - `add-ldap-simple`: Add new LDAP (simple auth) authentication source
+            - Options:
+                - `--name value`: Authentication name. Required.
+                - `--not-active`: Deactivate the authentication source.
+                - `--security-protocol value`: Security protocol name. Required.
+                - `--skip-tls-verify`: Disable TLS verification.
+                - `--host value`: The address where the LDAP server can be reached. Required.
+                - `--port value`: The port to use when connecting to the LDAP server. Required.
+                - `--user-search-base value`: The LDAP base at which user accounts will be searched for.
+                - `--user-filter value`: An LDAP filter declaring how to find the user record that is attempting to authenticate. Required.
+                - `--admin-filter value`: An LDAP filter specifying if a user should be given administrator privileges.
+                - `--username-attribute value`: The attribute of the user’s LDAP record containing the user name.
+                - `--firstname-attribute value`: The attribute of the user’s LDAP record containing the user’s first name.
+                - `--surname-attribute value`: The attribute of the user’s LDAP record containing the user’s surname.
+                - `--email-attribute value`: The attribute of the user’s LDAP record containing the user’s email address. Required.
+                - `--public-ssh-key-attribute value`: The attribute of the user’s LDAP record containing the user’s public ssh key.
+                - `--user-dn value`: The user’s DN. Required.
+            - Examples:
+                - `gitea admin auth add-ldap-simple --name ldap --security-protocol unencrypted --host mydomain.org --port 389 --user-dn "cn=%s,ou=Users,dc=mydomain,dc=org" --user-filter "(&(objectClass=posixAccount)(cn=%s))" --email-attribute mail`
+        - `update-ldap-simple`: Update existing LDAP (simple auth) authentication source
+            - Options:
+                - `--id value`: ID of authentication source. Required.
+                - `--name value`: Authentication name.
+                - `--not-active`: Deactivate the authentication source.
+                - `--security-protocol value`: Security protocol name.
+                - `--skip-tls-verify`: Disable TLS verification.
+                - `--host value`: The address where the LDAP server can be reached.
+                - `--port value`: The port to use when connecting to the LDAP server.
+                - `--user-search-base value`: The LDAP base at which user accounts will be searched for.
+                - `--user-filter value`: An LDAP filter declaring how to find the user record that is attempting to authenticate.
+                - `--admin-filter value`: An LDAP filter specifying if a user should be given administrator privileges.
+                - `--username-attribute value`: The attribute of the user’s LDAP record containing the user name.
+                - `--firstname-attribute value`: The attribute of the user’s LDAP record containing the user’s first name.
+                - `--surname-attribute value`: The attribute of the user’s LDAP record containing the user’s surname.
+                - `--email-attribute value`: The attribute of the user’s LDAP record containing the user’s email address.
+                - `--public-ssh-key-attribute value`: The attribute of the user’s LDAP record containing the user’s public ssh key.
+                - `--user-dn value`: The user’s DN.
+            - Examples:
+                - `gitea admin auth update-ldap-simple --id 1 --name "my ldap auth source"`
+                - `gitea admin auth update-ldap-simple --id 1 --username-attribute uid --firstname-attribute givenName --surname-attribute sn`
 
 #### cert