Browse Source

auth: support authentication source config file (#3142)

Unknwon 6 years ago
parent
commit
f2ecfdc96a

+ 1 - 1
Makefile

@@ -56,7 +56,7 @@ release: build pack
 bindata: pkg/bindata/bindata.go
 
 pkg/bindata/bindata.go: $(DATA_FILES)
-	go-bindata -o=$@ -ignore="\\.DS_Store|README.md|TRANSLATORS" -pkg=bindata conf/...
+	go-bindata -o=$@ -ignore="\\.DS_Store|README.md|TRANSLATORS|auth.d" -pkg=bindata conf/...
 
 less: public/css/gogs.css
 

+ 29 - 0
conf/auth.d/ldap_bind_dn.conf.example

@@ -0,0 +1,29 @@
+# This is an example of LDAP (BindDN) authentication
+#
+id           = 101
+type         = ldap_bind_dn
+name         = LDAP BindDN
+is_activated = true
+
+[config]
+host               = mydomain.com
+port               = 636
+# 0 - Unencrypted, 1 - LDAPS, 2 - StartTLS
+security_protocol  = 0
+skip_verify        = false
+bind_dn            = 
+bind_password      = 
+user_base          = ou=Users,dc=mydomain,dc=com
+attribute_username = 
+attribute_name     = 
+attribute_surname  = 
+attribute_mail     = mail
+attributes_in_bind = false
+filter             = (&(objectClass=posixAccount)(cn=%s))
+admin_filter       = 
+group_enabled      = false
+group_dn           = 
+group_filter       = 
+group_member_uid   = 
+user_uid           = 
+

+ 30 - 0
conf/auth.d/ldap_simple_auth.conf.example

@@ -0,0 +1,30 @@
+# This is an example of LDAP (simple auth) authentication
+#
+id           = 102
+type         = ldap_simple_auth
+name         = LDAP Simple Auth
+is_activated = true
+
+[config]
+host               = mydomain.com
+port               = 636
+# 0 - Unencrypted, 1 - LDAPS, 2 - StartTLS
+security_protocol  = 0
+skip_verify        = false
+bind_dn            = 
+bind_password      = 
+user_base          = 
+user_dn            = cn=%s,ou=Users,dc=mydomain,dc=com
+attribute_username = 
+attribute_name     = 
+attribute_surname  = 
+attribute_mail     = mail
+attributes_in_bind = false
+filter             = (&(objectClass=posixAccount)(cn=%s))
+admin_filter       = 
+group_enabled      = false
+group_dn           = 
+group_filter       = 
+group_member_uid   = 
+user_uid           = 
+

+ 10 - 0
conf/auth.d/pam.conf.example

@@ -0,0 +1,10 @@
+# This is an example of PAM authentication
+#
+id           = 104
+type         = pam
+name         = System Auth
+is_activated = true
+
+[config]
+service_name = system-auth
+

+ 16 - 0
conf/auth.d/smtp.conf.example

@@ -0,0 +1,16 @@
+# This is an example of SMTP authentication
+#
+id           = 103
+type         = smtp
+name         = GMail
+is_activated = true
+
+[config]
+# Either "PLAIN" or "LOGIN"
+auth            = PLAIN
+host            = smtp.gmail.com
+port            = 587
+allowed_domains = 
+tls             = true
+skip_verify     = false
+

+ 3 - 1
conf/locale/locale_en-US.ini

@@ -151,6 +151,8 @@ register_hepler_msg = Already have an account? Sign in now!
 social_register_hepler_msg = Already have an account? Bind now!
 disable_register_prompt = Sorry, registration has been disabled. Please contact the site administrator.
 disable_register_mail = Sorry, email services are disabled. Please contact the site administrator.
+auth_source = Authentication Source
+local = Local
 remember_me = Remember Me
 forgot_password= Forgot Password
 forget_password = Forgot password?
@@ -229,6 +231,7 @@ org_name_been_taken = Organization name has already been taken.
 team_name_been_taken = Team name has already been taken.
 email_been_used = Email address has already been used.
 username_password_incorrect = Username or password is not correct.
+auth_source_mismatch = The authentication source selected is not associated with the user.
 enterred_invalid_repo_name = Please make sure that the repository name you entered is correct.
 enterred_invalid_owner_name = Please make sure that the owner name you entered is correct.
 enterred_invalid_password = Please make sure the that password you entered is correct.
@@ -1128,7 +1131,6 @@ auths.enable_tls = Enable TLS Encryption
 auths.skip_tls_verify = Skip TLS Verify
 auths.pam_service_name = PAM Service Name
 auths.enable_auto_register = Enable Auto Registration
-auths.tips = Tips
 auths.edit = Edit Authentication Setting
 auths.activated = This authentication is activated
 auths.new_success = New authentication '%s' has been added successfully.

+ 0 - 13
models/error.go

@@ -505,19 +505,6 @@ func (err ErrAttachmentNotExist) Error() string {
 // |_______ \____/\___  /|__|___|  / /_______  /\____/|____/ |__|    \___  >___  >
 //         \/    /_____/         \/          \/                          \/    \/
 
-type ErrLoginSourceNotExist struct {
-	ID int64
-}
-
-func IsErrLoginSourceNotExist(err error) bool {
-	_, ok := err.(ErrLoginSourceNotExist)
-	return ok
-}
-
-func (err ErrLoginSourceNotExist) Error() string {
-	return fmt.Sprintf("login source does not exist [id: %d]", err.ID)
-}
-
 type ErrLoginSourceAlreadyExist struct {
 	Name string
 }

+ 27 - 0
models/errors/login_source.go

@@ -6,6 +6,19 @@ package errors
 
 import "fmt"
 
+type LoginSourceNotExist struct {
+	ID int64
+}
+
+func IsLoginSourceNotExist(err error) bool {
+	_, ok := err.(LoginSourceNotExist)
+	return ok
+}
+
+func (err LoginSourceNotExist) Error() string {
+	return fmt.Sprintf("login source does not exist [id: %d]", err.ID)
+}
+
 type LoginSourceNotActivated struct {
 	SourceID int64
 }
@@ -31,3 +44,17 @@ func IsInvalidLoginSourceType(err error) bool {
 func (err InvalidLoginSourceType) Error() string {
 	return fmt.Sprintf("invalid login source type [type: %v]", err.Type)
 }
+
+type LoginSourceMismatch struct {
+	Expect int64
+	Actual int64
+}
+
+func IsLoginSourceMismatch(err error) bool {
+	_, ok := err.(LoginSourceMismatch)
+	return ok
+}
+
+func (err LoginSourceMismatch) Error() string {
+	return fmt.Sprintf("login source mismatch [expect: %d, actual: %d]", err.Expect, err.Actual)
+}

+ 269 - 70
models/login_source.go

@@ -10,7 +10,10 @@ import (
 	"fmt"
 	"net/smtp"
 	"net/textproto"
+	"os"
+	"path"
 	"strings"
+	"sync"
 	"time"
 
 	"github.com/Unknwon/com"
@@ -18,10 +21,12 @@ import (
 	"github.com/go-xorm/core"
 	"github.com/go-xorm/xorm"
 	log "gopkg.in/clog.v1"
+	"gopkg.in/ini.v1"
 
 	"github.com/gogits/gogs/models/errors"
 	"github.com/gogits/gogs/pkg/auth/ldap"
 	"github.com/gogits/gogs/pkg/auth/pam"
+	"github.com/gogits/gogs/pkg/setting"
 )
 
 type LoginType int
@@ -57,7 +62,7 @@ var (
 )
 
 type LDAPConfig struct {
-	*ldap.Source
+	*ldap.Source `ini:"config"`
 }
 
 func (cfg *LDAPConfig) FromDB(bs []byte) error {
@@ -77,7 +82,7 @@ type SMTPConfig struct {
 	Host           string
 	Port           int
 	AllowedDomains string `xorm:"TEXT"`
-	TLS            bool
+	TLS            bool   `ini:"tls"`
 	SkipVerify     bool
 }
 
@@ -90,7 +95,7 @@ func (cfg *SMTPConfig) ToDB() ([]byte, error) {
 }
 
 type PAMConfig struct {
-	ServiceName string // pam service (e.g. system-auth)
+	ServiceName string // PAM service (e.g. system-auth)
 }
 
 func (cfg *PAMConfig) FromDB(bs []byte) error {
@@ -101,6 +106,27 @@ func (cfg *PAMConfig) ToDB() ([]byte, error) {
 	return json.Marshal(cfg)
 }
 
+// AuthSourceFile contains information of an authentication source file.
+type AuthSourceFile struct {
+	abspath string
+	file    *ini.File
+}
+
+// SetGeneral sets new value to the given key in the general (default) section.
+func (f *AuthSourceFile) SetGeneral(name, value string) {
+	f.file.Section("").Key(name).SetValue(value)
+}
+
+// SetConfig sets new values to the "config" section.
+func (f *AuthSourceFile) SetConfig(cfg core.Conversion) error {
+	return f.file.Section("config").ReflectFrom(cfg)
+}
+
+// Save writes updates into file system.
+func (f *AuthSourceFile) Save() error {
+	return f.file.SaveTo(f.abspath)
+}
+
 // LoginSource represents an external way for authorizing users.
 type LoginSource struct {
 	ID        int64
@@ -113,6 +139,8 @@ type LoginSource struct {
 	CreatedUnix int64
 	Updated     time.Time `xorm:"-"`
 	UpdatedUnix int64
+
+	LocalFile *AuthSourceFile `xorm:"-"`
 }
 
 func (s *LoginSource) BeforeInsert() {
@@ -135,16 +163,16 @@ func Cell2Int64(val xorm.Cell) int64 {
 	return (*val).(int64)
 }
 
-func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) {
+func (s *LoginSource) BeforeSet(colName string, val xorm.Cell) {
 	switch colName {
 	case "type":
 		switch LoginType(Cell2Int64(val)) {
 		case LOGIN_LDAP, LOGIN_DLDAP:
-			source.Cfg = new(LDAPConfig)
+			s.Cfg = new(LDAPConfig)
 		case LOGIN_SMTP:
-			source.Cfg = new(SMTPConfig)
+			s.Cfg = new(SMTPConfig)
 		case LOGIN_PAM:
-			source.Cfg = new(PAMConfig)
+			s.Cfg = new(PAMConfig)
 		default:
 			panic("unrecognized login source type: " + com.ToStr(*val))
 		}
@@ -160,65 +188,66 @@ func (s *LoginSource) AfterSet(colName string, _ xorm.Cell) {
 	}
 }
 
-func (source *LoginSource) TypeName() string {
-	return LoginNames[source.Type]
+func (s *LoginSource) TypeName() string {
+	return LoginNames[s.Type]
 }
 
-func (source *LoginSource) IsLDAP() bool {
-	return source.Type == LOGIN_LDAP
+func (s *LoginSource) IsLDAP() bool {
+	return s.Type == LOGIN_LDAP
 }
 
-func (source *LoginSource) IsDLDAP() bool {
-	return source.Type == LOGIN_DLDAP
+func (s *LoginSource) IsDLDAP() bool {
+	return s.Type == LOGIN_DLDAP
 }
 
-func (source *LoginSource) IsSMTP() bool {
-	return source.Type == LOGIN_SMTP
+func (s *LoginSource) IsSMTP() bool {
+	return s.Type == LOGIN_SMTP
 }
 
-func (source *LoginSource) IsPAM() bool {
-	return source.Type == LOGIN_PAM
+func (s *LoginSource) IsPAM() bool {
+	return s.Type == LOGIN_PAM
 }
 
-func (source *LoginSource) HasTLS() bool {
-	return ((source.IsLDAP() || source.IsDLDAP()) &&
-		source.LDAP().SecurityProtocol > ldap.SECURITY_PROTOCOL_UNENCRYPTED) ||
-		source.IsSMTP()
+func (s *LoginSource) HasTLS() bool {
+	return ((s.IsLDAP() || s.IsDLDAP()) &&
+		s.LDAP().SecurityProtocol > ldap.SECURITY_PROTOCOL_UNENCRYPTED) ||
+		s.IsSMTP()
 }
 
-func (source *LoginSource) UseTLS() bool {
-	switch source.Type {
+func (s *LoginSource) UseTLS() bool {
+	switch s.Type {
 	case LOGIN_LDAP, LOGIN_DLDAP:
-		return source.LDAP().SecurityProtocol != ldap.SECURITY_PROTOCOL_UNENCRYPTED
+		return s.LDAP().SecurityProtocol != ldap.SECURITY_PROTOCOL_UNENCRYPTED
 	case LOGIN_SMTP:
-		return source.SMTP().TLS
+		return s.SMTP().TLS
 	}
 
 	return false
 }
 
-func (source *LoginSource) SkipVerify() bool {
-	switch source.Type {
+func (s *LoginSource) SkipVerify() bool {
+	switch s.Type {
 	case LOGIN_LDAP, LOGIN_DLDAP:
-		return source.LDAP().SkipVerify
+		return s.LDAP().SkipVerify
 	case LOGIN_SMTP:
-		return source.SMTP().SkipVerify
+		return s.SMTP().SkipVerify
 	}
 
 	return false
 }
 
-func (source *LoginSource) LDAP() *LDAPConfig {
-	return source.Cfg.(*LDAPConfig)
+func (s *LoginSource) LDAP() *LDAPConfig {
+	return s.Cfg.(*LDAPConfig)
 }
 
-func (source *LoginSource) SMTP() *SMTPConfig {
-	return source.Cfg.(*SMTPConfig)
+func (s *LoginSource) SMTP() *SMTPConfig {
+	return s.Cfg.(*SMTPConfig)
 }
 
-func (source *LoginSource) PAM() *PAMConfig {
-	return source.Cfg.(*PAMConfig)
+func (s *LoginSource) PAM() *PAMConfig {
+	return s.Cfg.(*PAMConfig)
 }
+
 func CreateLoginSource(source *LoginSource) error {
 	has, err := x.Get(&LoginSource{Name: source.Name})
 	if err != nil {
@@ -231,9 +260,23 @@ func CreateLoginSource(source *LoginSource) error {
 	return err
 }
 
+// LoginSources returns all login sources defined.
 func LoginSources() ([]*LoginSource, error) {
-	auths := make([]*LoginSource, 0, 5)
-	return auths, x.Find(&auths)
+	sources := make([]*LoginSource, 0, 2)
+	if err := x.Find(&sources); err != nil {
+		return nil, err
+	}
+
+	return append(sources, localLoginSources.List()...), nil
+}
+
+// ActivatedLoginSources returns login sources that are currently activated.
+func ActivatedLoginSources() ([]*LoginSource, error) {
+	sources := make([]*LoginSource, 0, 2)
+	if err := x.Where("is_actived = ?", true).Find(&sources); err != nil {
+		return nil, fmt.Errorf("find activated login sources: %v", err)
+	}
+	return append(sources, localLoginSources.ActivatedList()...), nil
 }
 
 // GetLoginSourceByID returns login source by given ID.
@@ -243,14 +286,28 @@ func GetLoginSourceByID(id int64) (*LoginSource, error) {
 	if err != nil {
 		return nil, err
 	} else if !has {
-		return nil, ErrLoginSourceNotExist{id}
+		return localLoginSources.GetLoginSourceByID(id)
 	}
 	return source, nil
 }
 
-func UpdateSource(source *LoginSource) error {
-	_, err := x.Id(source.ID).AllCols().Update(source)
-	return err
+// UpdateLoginSource updates information of login source to database or local file.
+func UpdateLoginSource(source *LoginSource) error {
+	if source.LocalFile == nil {
+		_, err := x.Id(source.ID).AllCols().Update(source)
+		return err
+	}
+
+	source.LocalFile.SetGeneral("name", source.Name)
+	source.LocalFile.SetGeneral("is_activated", com.ToStr(source.IsActived))
+	if err := source.LocalFile.SetConfig(source.Cfg); err != nil {
+		return fmt.Errorf("LocalFile.SetConfig: %v", err)
+	} else if err = source.LocalFile.Save(); err != nil {
+		return fmt.Errorf("LocalFile.Save: %v", err)
+	}
+
+	localLoginSources.UpdateLoginSource(source)
+	return nil
 }
 
 func DeleteSource(source *LoginSource) error {
@@ -264,10 +321,151 @@ func DeleteSource(source *LoginSource) error {
 	return err
 }
 
-// CountLoginSources returns number of login sources.
+// CountLoginSources returns total number of login sources.
 func CountLoginSources() int64 {
 	count, _ := x.Count(new(LoginSource))
-	return count
+	return count + int64(localLoginSources.Len())
+}
+
+// LocalLoginSources contains authentication sources configured and loaded from local files.
+// Calling its methods is thread-safe; otherwise, please maintain the mutex accordingly.
+type LocalLoginSources struct {
+	sync.RWMutex
+	sources []*LoginSource
+}
+
+func (s *LocalLoginSources) Len() int {
+	return len(s.sources)
+}
+
+// List returns full clone of login sources.
+func (s *LocalLoginSources) List() []*LoginSource {
+	s.RLock()
+	defer s.RUnlock()
+
+	list := make([]*LoginSource, s.Len())
+	for i := range s.sources {
+		list[i] = &LoginSource{}
+		*list[i] = *s.sources[i]
+	}
+	return list
+}
+
+// ActivatedList returns clone of activated login sources.
+func (s *LocalLoginSources) ActivatedList() []*LoginSource {
+	s.RLock()
+	defer s.RUnlock()
+
+	list := make([]*LoginSource, 0, 2)
+	for i := range s.sources {
+		if !s.sources[i].IsActived {
+			continue
+		}
+
+		source := &LoginSource{}
+		*source = *s.sources[i]
+		list = append(list, source)
+	}
+	return list
+}
+
+// GetLoginSourceByID returns a clone of login source by given ID.
+func (s *LocalLoginSources) GetLoginSourceByID(id int64) (*LoginSource, error) {
+	s.RLock()
+	defer s.RUnlock()
+
+	for i := range s.sources {
+		if s.sources[i].ID == id {
+			source := &LoginSource{}
+			*source = *s.sources[i]
+			return source, nil
+		}
+	}
+
+	return nil, errors.LoginSourceNotExist{id}
+}
+
+// UpdateLoginSource updates in-memory copy of the authentication source.
+func (s *LocalLoginSources) UpdateLoginSource(source *LoginSource) {
+	s.Lock()
+	defer s.Unlock()
+
+	source.Updated = time.Now()
+	for i := range s.sources {
+		if s.sources[i].ID == source.ID {
+			*s.sources[i] = *source
+			break
+		}
+	}
+}
+
+var localLoginSources = &LocalLoginSources{}
+
+// LoadAuthSources loads authentication sources from local files
+// and converts them into login sources.
+func LoadAuthSources() {
+	authdPath := path.Join(setting.CustomPath, "conf/auth.d")
+	if !com.IsDir(authdPath) {
+		return
+	}
+
+	paths, err := com.GetFileListBySuffix(authdPath, ".conf")
+	if err != nil {
+		log.Fatal(2, "Failed to list authentication sources: %v", err)
+	}
+
+	localLoginSources.sources = make([]*LoginSource, 0, len(paths))
+
+	for _, fpath := range paths {
+		authSource, err := ini.Load(fpath)
+		if err != nil {
+			log.Fatal(2, "Failed to load authentication source: %v", err)
+		}
+		authSource.NameMapper = ini.TitleUnderscore
+
+		// Set general attributes
+		s := authSource.Section("")
+		loginSource := &LoginSource{
+			ID:        s.Key("id").MustInt64(),
+			Name:      s.Key("name").String(),
+			IsActived: s.Key("is_activated").MustBool(),
+			LocalFile: &AuthSourceFile{
+				abspath: fpath,
+				file:    authSource,
+			},
+		}
+
+		fi, err := os.Stat(fpath)
+		if err != nil {
+			log.Fatal(2, "Failed to load authentication source: %v", err)
+		}
+		loginSource.Updated = fi.ModTime()
+
+		// Parse authentication source file
+		authType := s.Key("type").String()
+		switch authType {
+		case "ldap_bind_dn":
+			loginSource.Type = LOGIN_LDAP
+			loginSource.Cfg = &LDAPConfig{}
+		case "ldap_simple_auth":
+			loginSource.Type = LOGIN_DLDAP
+			loginSource.Cfg = &LDAPConfig{}
+		case "smtp":
+			loginSource.Type = LOGIN_SMTP
+			loginSource.Cfg = &SMTPConfig{}
+		case "pam":
+			loginSource.Type = LOGIN_PAM
+			loginSource.Cfg = &PAMConfig{}
+		default:
+			log.Fatal(2, "Failed to load authentication source: unknown type '%s'", authType)
+		}
+
+		if err = authSource.Section("config").MapTo(loginSource.Cfg); err != nil {
+			log.Fatal(2, "Failed to parse authentication source 'config': %v", err)
+		}
+
+		localLoginSources.sources = append(localLoginSources.sources, loginSource)
+	}
 }
 
 // .____     ________      _____ __________
@@ -497,7 +695,7 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon
 	return user, CreateUser(user)
 }
 
-func ExternalUserLogin(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) {
+func remoteUserLogin(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) {
 	if !source.IsActived {
 		return nil, errors.LoginSourceNotActivated{source.ID}
 	}
@@ -514,8 +712,9 @@ func ExternalUserLogin(user *User, login, password string, source *LoginSource,
 	return nil, errors.InvalidLoginSourceType{source.Type}
 }
 
-// UserSignIn validates user name and password.
-func UserSignIn(username, password string) (*User, error) {
+// UserLogin validates user name and password via given login source ID.
+// If the loginSourceID is negative, it will abort login process if user is not found.
+func UserLogin(username, password string, loginSourceID int64) (*User, error) {
 	var user *User
 	if strings.Contains(username, "@") {
 		user = &User{Email: strings.ToLower(username)}
@@ -525,44 +724,44 @@ func UserSignIn(username, password string) (*User, error) {
 
 	hasUser, err := x.Get(user)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("get user record: %v", err)
 	}
 
 	if hasUser {
-		switch user.LoginType {
-		case LOGIN_NOTYPE, LOGIN_PLAIN:
+		// Note: This check is unnecessary but to reduce user confusion at login page
+		// and make it more consistent at user's perspective.
+		if loginSourceID >= 0 && user.LoginSource != loginSourceID {
+			return nil, errors.LoginSourceMismatch{loginSourceID, user.LoginSource}
+		}
+
+		// Validate password hash fetched from database for local accounts
+		if user.LoginType == LOGIN_NOTYPE ||
+			user.LoginType == LOGIN_PLAIN {
 			if user.ValidatePassword(password) {
 				return user, nil
 			}
 
 			return nil, errors.UserNotExist{user.ID, user.Name}
+		}
 
-		default:
-			var source LoginSource
-			hasSource, err := x.Id(user.LoginSource).Get(&source)
-			if err != nil {
-				return nil, err
-			} else if !hasSource {
-				return nil, ErrLoginSourceNotExist{user.LoginSource}
-			}
-
-			return ExternalUserLogin(user, user.LoginName, password, &source, false)
+		// Remote login to the login source the user is associated with
+		source, err := GetLoginSourceByID(user.LoginSource)
+		if err != nil {
+			return nil, err
 		}
-	}
 
-	sources := make([]*LoginSource, 0, 3)
-	if err = x.UseBool().Find(&sources, &LoginSource{IsActived: true}); err != nil {
-		return nil, err
+		return remoteUserLogin(user, user.LoginName, password, source, false)
 	}
 
-	for _, source := range sources {
-		authUser, err := ExternalUserLogin(nil, username, password, source, true)
-		if err == nil {
-			return authUser, nil
-		}
+	// Non-local login source is always greater than 0
+	if loginSourceID <= 0 {
+		return nil, errors.UserNotExist{-1, username}
+	}
 
-		log.Warn("Failed to login '%s' via '%s': %v", username, source.Name, err)
+	source, err := GetLoginSourceByID(loginSourceID)
+	if err != nil {
+		return nil, err
 	}
 
-	return nil, errors.UserNotExist{user.ID, user.Name}
+	return remoteUserLogin(nil, username, password, source, true)
 }

+ 2 - 2
pkg/auth/auth.go

@@ -127,10 +127,10 @@ func SignedInUser(ctx *macaron.Context, sess session.Store) (*models.User, bool)
 			if len(auths) == 2 && auths[0] == "Basic" {
 				uname, passwd, _ := tool.BasicAuthDecode(auths[1])
 
-				u, err := models.UserSignIn(uname, passwd)
+				u, err := models.UserLogin(uname, passwd, -1)
 				if err != nil {
 					if !errors.IsUserNotExist(err) {
-						log.Error(4, "UserSignIn: %v", err)
+						log.Error(4, "UserLogin: %v", err)
 					}
 					return nil, false
 				}

+ 7 - 10
pkg/auth/ldap/ldap.go

@@ -26,15 +26,14 @@ const (
 
 // Basic LDAP authentication service
 type Source struct {
-	Name              string // canonical name (ie. corporate.ad)
 	Host              string // LDAP host
 	Port              int    // port number
 	SecurityProtocol  SecurityProtocol
 	SkipVerify        bool
-	BindDN            string // DN to bind with
-	BindPassword      string // Bind DN password
-	UserBase          string // Base search path for users
-	UserDN            string // Template for the DN of the user for simple auth
+	BindDN            string `ini:"bind_dn,omitempty"` // DN to bind with
+	BindPassword      string `ini:",omitempty"`        // Bind DN password
+	UserBase          string `ini:",omitempty"`        // Base search path for users
+	UserDN            string `ini:"user_dn,omitempty"` // Template for the DN of the user for simple auth
 	AttributeUsername string // Username attribute
 	AttributeName     string // First name attribute
 	AttributeSurname  string // Surname attribute
@@ -43,11 +42,10 @@ type Source struct {
 	Filter            string // Query filter to validate entry
 	AdminFilter       string // Query filter to check if user is admin
 	GroupEnabled      bool   // if the group checking is enabled
-	GroupDN           string // Group Search Base
+	GroupDN           string `ini:"group_dn"` // Group Search Base
 	GroupFilter       string // Group Name Filter
-	GroupMemberUID    string // Group Attribute containing array of UserUID
-	UserUID           string // User Attribute listed in Group
-	Enabled           bool   // if this source is disabled
+	GroupMemberUID    string `ini:"group_member_uid"` // Group Attribute containing array of UserUID
+	UserUID           string `ini:"user_uid"`         // User Attribute listed in Group
 }
 
 func (ls *Source) sanitizedUserQuery(username string) (string, bool) {
@@ -186,7 +184,6 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
 	l, err := dial(ls)
 	if err != nil {
 		log.Error(2, "LDAP connect failed for '%s': %v", ls.Host, err)
-		ls.Enabled = false
 		return "", "", "", "", false, false
 	}
 	defer l.Close()

File diff suppressed because it is too large
+ 0 - 0
pkg/bindata/bindata.go


+ 4 - 3
pkg/form/user.go

@@ -74,9 +74,10 @@ func (f *Register) Validate(ctx *macaron.Context, errs binding.Errors) binding.E
 }
 
 type SignIn struct {
-	UserName string `binding:"Required;MaxSize(254)"`
-	Password string `binding:"Required;MaxSize(255)"`
-	Remember bool
+	UserName    string `binding:"Required;MaxSize(254)"`
+	Password    string `binding:"Required;MaxSize(255)"`
+	LoginSource int64
+	Remember    bool
 }
 
 func (f *SignIn) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {

+ 30 - 32
routes/admin/auths.go

@@ -25,19 +25,19 @@ const (
 )
 
 func Authentications(c *context.Context) {
-	c.Data["Title"] = c.Tr("admin.authentication")
-	c.Data["PageIsAdmin"] = true
-	c.Data["PageIsAdminAuthentications"] = true
+	c.Title("admin.authentication")
+	c.PageIs("Admin")
+	c.PageIs("AdminAuthentications")
 
 	var err error
 	c.Data["Sources"], err = models.LoginSources()
 	if err != nil {
-		c.Handle(500, "LoginSources", err)
+		c.ServerError("LoginSources", err)
 		return
 	}
 
 	c.Data["Total"] = models.CountLoginSources()
-	c.HTML(200, AUTHS)
+	c.Success(AUTHS)
 }
 
 type dropdownItem struct {
@@ -60,9 +60,9 @@ var (
 )
 
 func NewAuthSource(c *context.Context) {
-	c.Data["Title"] = c.Tr("admin.auths.new")
-	c.Data["PageIsAdmin"] = true
-	c.Data["PageIsAdminAuthentications"] = true
+	c.Title("admin.auths.new")
+	c.PageIs("Admin")
+	c.PageIs("AdminAuthentications")
 
 	c.Data["type"] = models.LOGIN_LDAP
 	c.Data["CurrentTypeName"] = models.LoginNames[models.LOGIN_LDAP]
@@ -72,13 +72,12 @@ func NewAuthSource(c *context.Context) {
 	c.Data["AuthSources"] = authSources
 	c.Data["SecurityProtocols"] = securityProtocols
 	c.Data["SMTPAuths"] = models.SMTPAuths
-	c.HTML(200, AUTH_NEW)
+	c.Success(AUTH_NEW)
 }
 
 func parseLDAPConfig(f form.Authentication) *models.LDAPConfig {
 	return &models.LDAPConfig{
 		Source: &ldap.Source{
-			Name:              f.Name,
 			Host:              f.Host,
 			Port:              f.Port,
 			SecurityProtocol:  ldap.SecurityProtocol(f.SecurityProtocol),
@@ -99,7 +98,6 @@ func parseLDAPConfig(f form.Authentication) *models.LDAPConfig {
 			GroupMemberUID:    f.GroupMemberUID,
 			UserUID:           f.UserUID,
 			AdminFilter:       f.AdminFilter,
-			Enabled:           true,
 		},
 	}
 }
@@ -116,9 +114,9 @@ func parseSMTPConfig(f form.Authentication) *models.SMTPConfig {
 }
 
 func NewAuthSourcePost(c *context.Context, f form.Authentication) {
-	c.Data["Title"] = c.Tr("admin.auths.new")
-	c.Data["PageIsAdmin"] = true
-	c.Data["PageIsAdminAuthentications"] = true
+	c.Title("admin.auths.new")
+	c.PageIs("Admin")
+	c.PageIs("AdminAuthentications")
 
 	c.Data["CurrentTypeName"] = models.LoginNames[models.LoginType(f.Type)]
 	c.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocol(f.SecurityProtocol)]
@@ -146,7 +144,7 @@ func NewAuthSourcePost(c *context.Context, f form.Authentication) {
 	c.Data["HasTLS"] = hasTLS
 
 	if c.HasError() {
-		c.HTML(200, AUTH_NEW)
+		c.Success(AUTH_NEW)
 		return
 	}
 
@@ -160,7 +158,7 @@ func NewAuthSourcePost(c *context.Context, f form.Authentication) {
 			c.Data["Err_Name"] = true
 			c.RenderWithErr(c.Tr("admin.auths.login_source_exist", err.(models.ErrLoginSourceAlreadyExist).Name), AUTH_NEW, f)
 		} else {
-			c.Handle(500, "CreateSource", err)
+			c.ServerError("CreateSource", err)
 		}
 		return
 	}
@@ -172,41 +170,41 @@ func NewAuthSourcePost(c *context.Context, f form.Authentication) {
 }
 
 func EditAuthSource(c *context.Context) {
-	c.Data["Title"] = c.Tr("admin.auths.edit")
-	c.Data["PageIsAdmin"] = true
-	c.Data["PageIsAdminAuthentications"] = true
+	c.Title("admin.auths.edit")
+	c.PageIs("Admin")
+	c.PageIs("AdminAuthentications")
 
 	c.Data["SecurityProtocols"] = securityProtocols
 	c.Data["SMTPAuths"] = models.SMTPAuths
 
 	source, err := models.GetLoginSourceByID(c.ParamsInt64(":authid"))
 	if err != nil {
-		c.Handle(500, "GetLoginSourceByID", err)
+		c.ServerError("GetLoginSourceByID", err)
 		return
 	}
 	c.Data["Source"] = source
 	c.Data["HasTLS"] = source.HasTLS()
 
-	c.HTML(200, AUTH_EDIT)
+	c.Success(AUTH_EDIT)
 }
 
 func EditAuthSourcePost(c *context.Context, f form.Authentication) {
-	c.Data["Title"] = c.Tr("admin.auths.edit")
-	c.Data["PageIsAdmin"] = true
-	c.Data["PageIsAdminAuthentications"] = true
+	c.Title("admin.auths.edit")
+	c.PageIs("Admin")
+	c.PageIs("AdminAuthentications")
 
 	c.Data["SMTPAuths"] = models.SMTPAuths
 
 	source, err := models.GetLoginSourceByID(c.ParamsInt64(":authid"))
 	if err != nil {
-		c.Handle(500, "GetLoginSourceByID", err)
+		c.ServerError("GetLoginSourceByID", err)
 		return
 	}
 	c.Data["Source"] = source
 	c.Data["HasTLS"] = source.HasTLS()
 
 	if c.HasError() {
-		c.HTML(200, AUTH_EDIT)
+		c.Success(AUTH_EDIT)
 		return
 	}
 
@@ -228,11 +226,11 @@ func EditAuthSourcePost(c *context.Context, f form.Authentication) {
 	source.Name = f.Name
 	source.IsActived = f.IsActive
 	source.Cfg = config
-	if err := models.UpdateSource(source); err != nil {
-		c.Handle(500, "UpdateSource", err)
+	if err := models.UpdateLoginSource(source); err != nil {
+		c.ServerError("UpdateLoginSource", err)
 		return
 	}
-	log.Trace("Authentication changed by admin(%s): %d", c.User.Name, source.ID)
+	log.Trace("Authentication changed by admin '%s': %d", c.User.Name, source.ID)
 
 	c.Flash.Success(c.Tr("admin.auths.update_success"))
 	c.Redirect(setting.AppSubURL + "/admin/auths/" + com.ToStr(f.ID))
@@ -241,7 +239,7 @@ func EditAuthSourcePost(c *context.Context, f form.Authentication) {
 func DeleteAuthSource(c *context.Context) {
 	source, err := models.GetLoginSourceByID(c.ParamsInt64(":authid"))
 	if err != nil {
-		c.Handle(500, "GetLoginSourceByID", err)
+		c.ServerError("GetLoginSourceByID", err)
 		return
 	}
 
@@ -251,7 +249,7 @@ func DeleteAuthSource(c *context.Context) {
 		} else {
 			c.Flash.Error(fmt.Sprintf("DeleteSource: %v", err))
 		}
-		c.JSON(200, map[string]interface{}{
+		c.JSONSuccess(map[string]interface{}{
 			"redirect": setting.AppSubURL + "/admin/auths/" + c.Params(":authid"),
 		})
 		return
@@ -259,7 +257,7 @@ func DeleteAuthSource(c *context.Context) {
 	log.Trace("Authentication deleted by admin(%s): %d", c.User.Name, source.ID)
 
 	c.Flash.Success(c.Tr("admin.auths.deletion_success"))
-	c.JSON(200, map[string]interface{}{
+	c.JSONSuccess(map[string]interface{}{
 		"redirect": setting.AppSubURL + "/admin/auths",
 	})
 }

+ 2 - 1
routes/api/v1/admin/user.go

@@ -10,6 +10,7 @@ import (
 	api "github.com/gogits/go-gogs-client"
 
 	"github.com/gogits/gogs/models"
+	"github.com/gogits/gogs/models/errors"
 	"github.com/gogits/gogs/pkg/context"
 	"github.com/gogits/gogs/pkg/mailer"
 	"github.com/gogits/gogs/pkg/setting"
@@ -23,7 +24,7 @@ func parseLoginSource(c *context.APIContext, u *models.User, sourceID int64, log
 
 	source, err := models.GetLoginSourceByID(sourceID)
 	if err != nil {
-		if models.IsErrLoginSourceNotExist(err) {
+		if errors.IsLoginSourceNotExist(err) {
 			c.Error(422, "", err)
 		} else {
 			c.Error(500, "GetLoginSourceByID", err)

+ 1 - 0
routes/install.go

@@ -67,6 +67,7 @@ func GlobalInit() {
 		}
 		models.HasEngine = true
 
+		models.LoadAuthSources()
 		models.LoadRepoConfig()
 		models.NewRepoContext()
 

+ 6 - 6
routes/org/setting.go

@@ -107,16 +107,16 @@ func SettingsDeleteAvatar(c *context.Context) {
 }
 
 func SettingsDelete(c *context.Context) {
-	c.Data["Title"] = c.Tr("org.settings")
-	c.Data["PageIsSettingsDelete"] = true
+	c.Title("org.settings")
+	c.PageIs("SettingsDelete")
 
 	org := c.Org.Organization
 	if c.Req.Method == "POST" {
-		if _, err := models.UserSignIn(c.User.Name, c.Query("password")); err != nil {
+		if _, err := models.UserLogin(c.User.Name, c.Query("password"), c.User.LoginSource); err != nil {
 			if errors.IsUserNotExist(err) {
 				c.RenderWithErr(c.Tr("form.enterred_invalid_password"), SETTINGS_DELETE, nil)
 			} else {
-				c.Handle(500, "UserSignIn", err)
+				c.ServerError("UserLogin", err)
 			}
 			return
 		}
@@ -126,7 +126,7 @@ func SettingsDelete(c *context.Context) {
 				c.Flash.Error(c.Tr("form.org_still_own_repo"))
 				c.Redirect(c.Org.OrgLink + "/settings/delete")
 			} else {
-				c.Handle(500, "DeleteOrganization", err)
+				c.ServerError("DeleteOrganization", err)
 			}
 		} else {
 			log.Trace("Organization deleted: %s", org.Name)
@@ -135,7 +135,7 @@ func SettingsDelete(c *context.Context) {
 		return
 	}
 
-	c.HTML(200, SETTINGS_DELETE)
+	c.Success(SETTINGS_DELETE)
 }
 
 func Webhooks(c *context.Context) {

+ 2 - 2
routes/repo/http.go

@@ -124,9 +124,9 @@ func HTTPContexter() macaron.Handler {
 			return
 		}
 
-		authUser, err := models.UserSignIn(authUsername, authPassword)
+		authUser, err := models.UserLogin(authUsername, authPassword, -1)
 		if err != nil && !errors.IsUserNotExist(err) {
-			c.Handle(http.StatusInternalServerError, "UserSignIn", err)
+			c.Handle(http.StatusInternalServerError, "UserLogin", err)
 			return
 		}
 

+ 31 - 9
routes/user/auth.go

@@ -80,12 +80,12 @@ func isValidRedirect(url string) bool {
 }
 
 func Login(c *context.Context) {
-	c.Data["Title"] = c.Tr("sign_in")
+	c.Title("sign_in")
 
-	// Check auto-login.
+	// Check auto-login
 	isSucceed, err := AutoLogin(c)
 	if err != nil {
-		c.Handle(500, "AutoLogin", err)
+		c.ServerError("AutoLogin", err)
 		return
 	}
 
@@ -106,7 +106,15 @@ func Login(c *context.Context) {
 		return
 	}
 
-	c.HTML(200, LOGIN)
+	// Display normal login page
+	loginSources, err := models.ActivatedLoginSources()
+	if err != nil {
+		c.ServerError("ActivatedLoginSources", err)
+		return
+	}
+	c.Data["LoginSources"] = loginSources
+
+	c.Success(LOGIN)
 }
 
 func afterLogin(c *context.Context, u *models.User, remember bool) {
@@ -138,19 +146,33 @@ func afterLogin(c *context.Context, u *models.User, remember bool) {
 }
 
 func LoginPost(c *context.Context, f form.SignIn) {
-	c.Data["Title"] = c.Tr("sign_in")
+	c.Title("sign_in")
+
+	loginSources, err := models.ActivatedLoginSources()
+	if err != nil {
+		c.ServerError("ActivatedLoginSources", err)
+		return
+	}
+	c.Data["LoginSources"] = loginSources
 
 	if c.HasError() {
 		c.Success(LOGIN)
 		return
 	}
 
-	u, err := models.UserSignIn(f.UserName, f.Password)
+	u, err := models.UserLogin(f.UserName, f.Password, f.LoginSource)
 	if err != nil {
-		if errors.IsUserNotExist(err) {
+		switch err.(type) {
+		case errors.UserNotExist:
+			c.FormErr("UserName")
+			c.FormErr("Password")
 			c.RenderWithErr(c.Tr("form.username_password_incorrect"), LOGIN, &f)
-		} else {
-			c.ServerError("UserSignIn", err)
+		case errors.LoginSourceMismatch:
+			c.FormErr("LoginSource")
+			c.RenderWithErr(c.Tr("form.auth_source_mismatch"), LOGIN, &f)
+
+		default:
+			c.ServerError("UserLogin", err)
 		}
 		return
 	}

+ 2 - 2
routes/user/setting.go

@@ -633,11 +633,11 @@ func SettingsDelete(c *context.Context) {
 	c.PageIs("SettingsDelete")
 
 	if c.Req.Method == "POST" {
-		if _, err := models.UserSignIn(c.User.Name, c.Query("password")); err != nil {
+		if _, err := models.UserLogin(c.User.Name, c.Query("password"), c.User.LoginSource); err != nil {
 			if errors.IsUserNotExist(err) {
 				c.RenderWithErr(c.Tr("form.enterred_invalid_password"), SETTINGS_DELETE, nil)
 			} else {
-				c.ServerError("UserSignIn", err)
+				c.ServerError("UserLogin", err)
 			}
 			return
 		}

+ 3 - 1
templates/admin/auth/edit.tmpl

@@ -189,7 +189,9 @@
 
 						<div class="field">
 							<button class="ui green button">{{.i18n.Tr "admin.auths.update"}}</button>
-							<div class="ui red button delete-button" data-url="{{$.Link}}/delete" data-id="{{.Source.ID}}">{{.i18n.Tr "admin.auths.delete"}}</div>
+							{{if not .Source.LocalFile}}
+								<div class="ui red button delete-button" data-url="{{$.Link}}/delete" data-id="{{.Source.ID}}">{{.i18n.Tr "admin.auths.delete"}}</div>
+							{{end}}
 						</div>
 					</form>
 				</div>

+ 7 - 1
templates/admin/auth/list.tmpl

@@ -29,7 +29,13 @@
 									<td>{{.TypeName}}</td>
 									<td><i class="fa fa{{if .IsActived}}-check{{end}}-square-o"></i></td>
 									<td><span class="poping up" data-content="{{DateFmtLong .Updated}}" data-variation="tiny">{{DateFmtShort .Updated}}</span></td>
-									<td><span class="poping up" data-content="{{DateFmtLong .Created}}" data-variation="tiny">{{DateFmtShort .Created}}</span></td>
+									<td>
+										{{if .Created.IsZero}}
+											N/A
+										{{else}}
+											<span class="poping up" data-content="{{DateFmtLong .Created}}" data-variation="tiny">{{DateFmtShort .Created}}</span>
+										{{end}}
+									</td>
 									<td><a href="{{AppSubURL}}/admin/auths/{{.ID}}"><i class="fa fa-pencil-square-o"></i></a></td>
 								</tr>
 							{{end}}

+ 0 - 8
templates/admin/auth/new.tmpl

@@ -188,14 +188,6 @@
 						</div>
 					</form>
 				</div>
-
-				<h4 class="ui top attached header">
-					{{.i18n.Tr "admin.auths.tips"}}
-				</h4>
-				<div class="ui attached segment">
-					<h5>GMail Settings:</h5>
-					<p>Host: smtp.gmail.com, Port: 587, Enable TLS Encryption: true</p>
-				</div>
 			</div>
 		</div>
 	</div>

+ 18 - 0
templates/user/auth/login.tmpl

@@ -17,6 +17,24 @@
 						<label for="password">{{.i18n.Tr "password"}}</label>
 						<input id="password" name="password" type="password" autocomplete="off" value="{{.password}}" required>
 					</div>
+					{{if .LoginSources}}
+						<div class="required inline field {{if .Err_LoginSource}}error{{end}}">
+							<label for="password">{{.i18n.Tr "auth.auth_source"}}</label>
+							<div class="ui selection dropdown">
+								<input type="hidden" id="login_source" name="login_source" value="{{.login_source}}" required>
+								<span class="text">
+									{{.i18n.Tr "auth.local"}}
+								</span>
+								<i class="dropdown icon"></i>
+								<div class="menu">
+									<div class="item" data-value="0">{{.i18n.Tr "auth.local"}}</div>
+									{{range .LoginSources}}
+										<div class="item" data-value="{{.ID}}">{{.Name}}</div>
+									{{end}}
+								</div>
+							</div>
+						</div>
+					{{end}}
 					<div class="inline field">
 						<label></label>
 						<div class="ui checkbox">

Some files were not shown because too many files changed in this diff