From: Unknwon Date: Wed, 31 Aug 2016 08:22:41 +0000 (-0700) Subject: models/login_source: code improvement X-Git-Tag: v0.9.99~18 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=152e715999bfec9859423034aec3df90442961f1;p=gitea.git models/login_source: code improvement --- diff --git a/README.md b/README.md index cab3c59d45..60aa87527b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Gogs - Go Git Service [![Build Status](https://travis-ci.org/gogits/gogs.svg?bra ![](https://github.com/gogits/gogs/blob/master/public/img/gogs-large-resize.png?raw=true) -##### Current tip version: 0.9.96 (see [Releases](https://github.com/gogits/gogs/releases) for binary versions) +##### Current tip version: 0.9.97 (see [Releases](https://github.com/gogits/gogs/releases) for binary versions) | Web | UI | Preview | |:-------------:|:-------:|:-------:| diff --git a/gogs.go b/gogs.go index be61885dc3..04689d4544 100644 --- a/gogs.go +++ b/gogs.go @@ -17,7 +17,7 @@ import ( "github.com/gogits/gogs/modules/setting" ) -const APP_VER = "0.9.96.0830" +const APP_VER = "0.9.97.0830" func init() { runtime.GOMAXPROCS(runtime.NumCPU()) diff --git a/models/error.go b/models/error.go index 065857e001..182a944a6f 100644 --- a/models/error.go +++ b/models/error.go @@ -635,6 +635,19 @@ func (err ErrLoginSourceAlreadyExist) Error() string { return fmt.Sprintf("login source already exists [name: %s]", err.Name) } +type ErrLoginSourceInUse struct { + ID int64 +} + +func IsErrLoginSourceInUse(err error) bool { + _, ok := err.(ErrLoginSourceInUse) + return ok +} + +func (err ErrLoginSourceInUse) Error() string { + return fmt.Sprintf("login source is still used by some users [id: %d]", err.ID) +} + // ___________ // \__ ___/___ _____ _____ // | |_/ __ \\__ \ / \ diff --git a/models/login.go b/models/login.go deleted file mode 100644 index bef1617dd6..0000000000 --- a/models/login.go +++ /dev/null @@ -1,573 +0,0 @@ -// Copyright 2014 The Gogs 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 models - -import ( - "crypto/tls" - "encoding/json" - "errors" - "fmt" - "net/smtp" - "net/textproto" - "strings" - "time" - - "github.com/Unknwon/com" - "github.com/go-macaron/binding" - "github.com/go-xorm/core" - "github.com/go-xorm/xorm" - - "github.com/gogits/gogs/modules/auth/ldap" - "github.com/gogits/gogs/modules/auth/pam" - "github.com/gogits/gogs/modules/log" -) - -var ( - ErrAuthenticationUserUsed = errors.New("Authentication has been used by some users") -) - -type LoginType int - -// Note: new type must be added at the end of list to maintain compatibility. -const ( - LOGIN_NOTYPE LoginType = iota - LOGIN_PLAIN // 1 - LOGIN_LDAP // 2 - LOGIN_SMTP // 3 - LOGIN_PAM // 4 - LOGIN_DLDAP // 5 -) - -var LoginNames = map[LoginType]string{ - LOGIN_LDAP: "LDAP (via BindDN)", - LOGIN_DLDAP: "LDAP (simple auth)", // Via direct bind - LOGIN_SMTP: "SMTP", - LOGIN_PAM: "PAM", -} - -var SecurityProtocolNames = map[ldap.SecurityProtocol]string{ - ldap.SECURITY_PROTOCOL_UNENCRYPTED: "Unencrypted", - ldap.SECURITY_PROTOCOL_LDAPS: "LDAPS", - ldap.SECURITY_PROTOCOL_START_TLS: "StartTLS", -} - -// Ensure structs implemented interface. -var ( - _ core.Conversion = &LDAPConfig{} - _ core.Conversion = &SMTPConfig{} - _ core.Conversion = &PAMConfig{} -) - -type LDAPConfig struct { - *ldap.Source -} - -func (cfg *LDAPConfig) FromDB(bs []byte) error { - return json.Unmarshal(bs, &cfg) -} - -func (cfg *LDAPConfig) ToDB() ([]byte, error) { - return json.Marshal(cfg) -} - -func (cfg *LDAPConfig) SecurityProtocolName() string { - return SecurityProtocolNames[cfg.SecurityProtocol] -} - -type SMTPConfig struct { - Auth string - Host string - Port int - AllowedDomains string `xorm:"TEXT"` - TLS bool - SkipVerify bool -} - -func (cfg *SMTPConfig) FromDB(bs []byte) error { - return json.Unmarshal(bs, cfg) -} - -func (cfg *SMTPConfig) ToDB() ([]byte, error) { - return json.Marshal(cfg) -} - -type PAMConfig struct { - ServiceName string // pam service (e.g. system-auth) -} - -func (cfg *PAMConfig) FromDB(bs []byte) error { - return json.Unmarshal(bs, &cfg) -} - -func (cfg *PAMConfig) ToDB() ([]byte, error) { - return json.Marshal(cfg) -} - -type LoginSource struct { - ID int64 `xorm:"pk autoincr"` - Type LoginType - Name string `xorm:"UNIQUE"` - IsActived bool `xorm:"NOT NULL DEFAULT false"` - Cfg core.Conversion `xorm:"TEXT"` - - Created time.Time `xorm:"-"` - CreatedUnix int64 - Updated time.Time `xorm:"-"` - UpdatedUnix int64 -} - -func (s *LoginSource) BeforeInsert() { - s.CreatedUnix = time.Now().Unix() - s.UpdatedUnix = s.CreatedUnix -} - -func (s *LoginSource) BeforeUpdate() { - s.UpdatedUnix = time.Now().Unix() -} - -// Cell2Int64 converts a xorm.Cell type to int64, -// and handles possible irregular cases. -func Cell2Int64(val xorm.Cell) int64 { - switch (*val).(type) { - case []uint8: - log.Trace("Cell2Int64 ([]uint8): %v", *val) - return com.StrTo(string((*val).([]uint8))).MustInt64() - } - return (*val).(int64) -} - -func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) { - switch colName { - case "type": - switch LoginType(Cell2Int64(val)) { - case LOGIN_LDAP, LOGIN_DLDAP: - source.Cfg = new(LDAPConfig) - case LOGIN_SMTP: - source.Cfg = new(SMTPConfig) - case LOGIN_PAM: - source.Cfg = new(PAMConfig) - default: - panic("unrecognized login source type: " + com.ToStr(*val)) - } - } -} - -func (s *LoginSource) AfterSet(colName string, _ xorm.Cell) { - switch colName { - case "created_unix": - s.Created = time.Unix(s.CreatedUnix, 0).Local() - case "updated_unix": - s.Updated = time.Unix(s.UpdatedUnix, 0).Local() - } -} - -func (source *LoginSource) TypeName() string { - return LoginNames[source.Type] -} - -func (source *LoginSource) IsLDAP() bool { - return source.Type == LOGIN_LDAP -} - -func (source *LoginSource) IsDLDAP() bool { - return source.Type == LOGIN_DLDAP -} - -func (source *LoginSource) IsSMTP() bool { - return source.Type == LOGIN_SMTP -} - -func (source *LoginSource) IsPAM() bool { - return source.Type == LOGIN_PAM -} - -func (source *LoginSource) HasTLS() bool { - return ((source.IsLDAP() || source.IsDLDAP()) && - source.LDAP().SecurityProtocol > ldap.SECURITY_PROTOCOL_UNENCRYPTED) || - source.IsSMTP() -} - -func (source *LoginSource) UseTLS() bool { - switch source.Type { - case LOGIN_LDAP, LOGIN_DLDAP: - return source.LDAP().SecurityProtocol != ldap.SECURITY_PROTOCOL_UNENCRYPTED - case LOGIN_SMTP: - return source.SMTP().TLS - } - - return false -} - -func (source *LoginSource) SkipVerify() bool { - switch source.Type { - case LOGIN_LDAP, LOGIN_DLDAP: - return source.LDAP().SkipVerify - case LOGIN_SMTP: - return source.SMTP().SkipVerify - } - - return false -} - -func (source *LoginSource) LDAP() *LDAPConfig { - return source.Cfg.(*LDAPConfig) -} - -func (source *LoginSource) SMTP() *SMTPConfig { - return source.Cfg.(*SMTPConfig) -} - -func (source *LoginSource) PAM() *PAMConfig { - return source.Cfg.(*PAMConfig) -} - -// CountLoginSources returns number of login sources. -func CountLoginSources() int64 { - count, _ := x.Count(new(LoginSource)) - return count -} - -func CreateLoginSource(source *LoginSource) error { - has, err := x.Get(&LoginSource{Name: source.Name}) - if err != nil { - return err - } else if has { - return ErrLoginSourceAlreadyExist{source.Name} - } - - _, err = x.Insert(source) - return err -} - -func LoginSources() ([]*LoginSource, error) { - auths := make([]*LoginSource, 0, 5) - return auths, x.Find(&auths) -} - -// GetLoginSourceByID returns login source by given ID. -func GetLoginSourceByID(id int64) (*LoginSource, error) { - source := new(LoginSource) - has, err := x.Id(id).Get(source) - if err != nil { - return nil, err - } else if !has { - return nil, ErrLoginSourceNotExist{id} - } - return source, nil -} - -func UpdateSource(source *LoginSource) error { - _, err := x.Id(source.ID).AllCols().Update(source) - return err -} - -func DeleteSource(source *LoginSource) error { - count, err := x.Count(&User{LoginSource: source.ID}) - if err != nil { - return err - } else if count > 0 { - return ErrAuthenticationUserUsed - } - _, err = x.Id(source.ID).Delete(new(LoginSource)) - return err -} - -// .____ ________ _____ __________ -// | | \______ \ / _ \\______ \ -// | | | | \ / /_\ \| ___/ -// | |___ | ` \/ | \ | -// |_______ \/_______ /\____|__ /____| -// \/ \/ \/ - -// LoginUserLDAPSource queries if loginName/passwd can login against the LDAP directory pool, -// and create a local user if success when enabled. -// It returns the same LoginUserPlain semantic. -func LoginUserLDAPSource(u *User, loginName, passwd string, source *LoginSource, autoRegister bool) (*User, error) { - cfg := source.Cfg.(*LDAPConfig) - directBind := (source.Type == LOGIN_DLDAP) - username, fn, sn, mail, isAdmin, logged := cfg.SearchEntry(loginName, passwd, directBind) - if !logged { - // User not in LDAP, do nothing - return nil, ErrUserNotExist{0, loginName} - } - - if !autoRegister { - return u, nil - } - - // Fallback. - if len(username) == 0 { - username = loginName - } - // Validate username make sure it satisfies requirement. - if binding.AlphaDashDotPattern.MatchString(username) { - return nil, fmt.Errorf("Invalid pattern for attribute 'username' [%s]: must be valid alpha or numeric or dash(-_) or dot characters", username) - } - - if len(mail) == 0 { - mail = fmt.Sprintf("%s@localhost", username) - } - - u = &User{ - LowerName: strings.ToLower(username), - Name: username, - FullName: composeFullName(fn, sn, username), - LoginType: source.Type, - LoginSource: source.ID, - LoginName: loginName, - Email: mail, - IsAdmin: isAdmin, - IsActive: true, - } - return u, CreateUser(u) -} - -func composeFullName(firstname, surname, username string) string { - switch { - case len(firstname) == 0 && len(surname) == 0: - return username - case len(firstname) == 0: - return surname - case len(surname) == 0: - return firstname - default: - return firstname + " " + surname - } -} - -// _________ __________________________ -// / _____/ / \__ ___/\______ \ -// \_____ \ / \ / \| | | ___/ -// / \/ Y \ | | | -// /_______ /\____|__ /____| |____| -// \/ \/ - -type loginAuth struct { - username, password string -} - -func LoginAuth(username, password string) smtp.Auth { - return &loginAuth{username, password} -} - -func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { - return "LOGIN", []byte(a.username), nil -} - -func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { - if more { - switch string(fromServer) { - case "Username:": - return []byte(a.username), nil - case "Password:": - return []byte(a.password), nil - } - } - return nil, nil -} - -const ( - SMTP_PLAIN = "PLAIN" - SMTP_LOGIN = "LOGIN" -) - -var SMTPAuths = []string{SMTP_PLAIN, SMTP_LOGIN} - -func SMTPAuth(a smtp.Auth, cfg *SMTPConfig) error { - c, err := smtp.Dial(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)) - if err != nil { - return err - } - defer c.Close() - - if err = c.Hello("gogs"); err != nil { - return err - } - - if cfg.TLS { - if ok, _ := c.Extension("STARTTLS"); ok { - if err = c.StartTLS(&tls.Config{ - InsecureSkipVerify: cfg.SkipVerify, - ServerName: cfg.Host, - }); err != nil { - return err - } - } else { - return errors.New("SMTP server unsupports TLS") - } - } - - if ok, _ := c.Extension("AUTH"); ok { - if err = c.Auth(a); err != nil { - return err - } - return nil - } - return ErrUnsupportedLoginType -} - -// Query if name/passwd can login against the LDAP directory pool -// Create a local user if success -// Return the same LoginUserPlain semantic -func LoginUserSMTPSource(u *User, name, passwd string, sourceID int64, cfg *SMTPConfig, autoRegister bool) (*User, error) { - // Verify allowed domains. - if len(cfg.AllowedDomains) > 0 { - idx := strings.Index(name, "@") - if idx == -1 { - return nil, ErrUserNotExist{0, name} - } else if !com.IsSliceContainsStr(strings.Split(cfg.AllowedDomains, ","), name[idx+1:]) { - return nil, ErrUserNotExist{0, name} - } - } - - var auth smtp.Auth - if cfg.Auth == SMTP_PLAIN { - auth = smtp.PlainAuth("", name, passwd, cfg.Host) - } else if cfg.Auth == SMTP_LOGIN { - auth = LoginAuth(name, passwd) - } else { - return nil, errors.New("Unsupported SMTP auth type") - } - - if err := SMTPAuth(auth, cfg); err != nil { - // Check standard error format first, - // then fallback to worse case. - tperr, ok := err.(*textproto.Error) - if (ok && tperr.Code == 535) || - strings.Contains(err.Error(), "Username and Password not accepted") { - return nil, ErrUserNotExist{0, name} - } - return nil, err - } - - if !autoRegister { - return u, nil - } - - var loginName = name - idx := strings.Index(name, "@") - if idx > -1 { - loginName = name[:idx] - } - // fake a local user creation - u = &User{ - LowerName: strings.ToLower(loginName), - Name: strings.ToLower(loginName), - LoginType: LOGIN_SMTP, - LoginSource: sourceID, - LoginName: name, - IsActive: true, - Passwd: passwd, - Email: name, - } - err := CreateUser(u) - return u, err -} - -// __________ _____ _____ -// \______ \/ _ \ / \ -// | ___/ /_\ \ / \ / \ -// | | / | \/ Y \ -// |____| \____|__ /\____|__ / -// \/ \/ - -// Query if name/passwd can login against PAM -// Create a local user if success -// Return the same LoginUserPlain semantic -func LoginUserPAMSource(u *User, name, passwd string, sourceID int64, cfg *PAMConfig, autoRegister bool) (*User, error) { - if err := pam.PAMAuth(cfg.ServiceName, name, passwd); err != nil { - if strings.Contains(err.Error(), "Authentication failure") { - return nil, ErrUserNotExist{0, name} - } - return nil, err - } - - if !autoRegister { - return u, nil - } - - // fake a local user creation - u = &User{ - LowerName: strings.ToLower(name), - Name: name, - LoginType: LOGIN_PAM, - LoginSource: sourceID, - LoginName: name, - IsActive: true, - Passwd: passwd, - Email: name, - } - return u, CreateUser(u) -} - -func ExternalUserLogin(u *User, name, passwd string, source *LoginSource, autoRegister bool) (*User, error) { - if !source.IsActived { - return nil, ErrLoginSourceNotActived - } - - switch source.Type { - case LOGIN_LDAP, LOGIN_DLDAP: - return LoginUserLDAPSource(u, name, passwd, source, autoRegister) - case LOGIN_SMTP: - return LoginUserSMTPSource(u, name, passwd, source.ID, source.Cfg.(*SMTPConfig), autoRegister) - case LOGIN_PAM: - return LoginUserPAMSource(u, name, passwd, source.ID, source.Cfg.(*PAMConfig), autoRegister) - } - - return nil, ErrUnsupportedLoginType -} - -// UserSignIn validates user name and password. -func UserSignIn(uname, passwd string) (*User, error) { - var u *User - if strings.Contains(uname, "@") { - u = &User{Email: strings.ToLower(uname)} - } else { - u = &User{LowerName: strings.ToLower(uname)} - } - - userExists, err := x.Get(u) - if err != nil { - return nil, err - } - - if userExists { - switch u.LoginType { - case LOGIN_NOTYPE, LOGIN_PLAIN: - if u.ValidatePassword(passwd) { - return u, nil - } - - return nil, ErrUserNotExist{u.ID, u.Name} - - default: - var source LoginSource - hasSource, err := x.Id(u.LoginSource).Get(&source) - if err != nil { - return nil, err - } else if !hasSource { - return nil, ErrLoginSourceNotExist{u.LoginSource} - } - - return ExternalUserLogin(u, u.LoginName, passwd, &source, false) - } - } - - var sources []LoginSource - if err = x.UseBool().Find(&sources, &LoginSource{IsActived: true}); err != nil { - return nil, err - } - - for _, source := range sources { - u, err := ExternalUserLogin(nil, uname, passwd, &source, true) - if err == nil { - return u, nil - } - - log.Warn("Failed to login '%s' via '%s': %v", uname, source.Name, err) - } - - return nil, ErrUserNotExist{u.ID, u.Name} -} diff --git a/models/login_source.go b/models/login_source.go new file mode 100644 index 0000000000..82186fb713 --- /dev/null +++ b/models/login_source.go @@ -0,0 +1,558 @@ +// Copyright 2014 The Gogs 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 models + +import ( + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "net/smtp" + "net/textproto" + "strings" + "time" + + "github.com/Unknwon/com" + "github.com/go-macaron/binding" + "github.com/go-xorm/core" + "github.com/go-xorm/xorm" + + "github.com/gogits/gogs/modules/auth/ldap" + "github.com/gogits/gogs/modules/auth/pam" + "github.com/gogits/gogs/modules/log" +) + +type LoginType int + +// Note: new type must append to the end of list to maintain compatibility. +const ( + LOGIN_NOTYPE LoginType = iota + LOGIN_PLAIN // 1 + LOGIN_LDAP // 2 + LOGIN_SMTP // 3 + LOGIN_PAM // 4 + LOGIN_DLDAP // 5 +) + +var LoginNames = map[LoginType]string{ + LOGIN_LDAP: "LDAP (via BindDN)", + LOGIN_DLDAP: "LDAP (simple auth)", // Via direct bind + LOGIN_SMTP: "SMTP", + LOGIN_PAM: "PAM", +} + +var SecurityProtocolNames = map[ldap.SecurityProtocol]string{ + ldap.SECURITY_PROTOCOL_UNENCRYPTED: "Unencrypted", + ldap.SECURITY_PROTOCOL_LDAPS: "LDAPS", + ldap.SECURITY_PROTOCOL_START_TLS: "StartTLS", +} + +// Ensure structs implemented interface. +var ( + _ core.Conversion = &LDAPConfig{} + _ core.Conversion = &SMTPConfig{} + _ core.Conversion = &PAMConfig{} +) + +type LDAPConfig struct { + *ldap.Source +} + +func (cfg *LDAPConfig) FromDB(bs []byte) error { + return json.Unmarshal(bs, &cfg) +} + +func (cfg *LDAPConfig) ToDB() ([]byte, error) { + return json.Marshal(cfg) +} + +func (cfg *LDAPConfig) SecurityProtocolName() string { + return SecurityProtocolNames[cfg.SecurityProtocol] +} + +type SMTPConfig struct { + Auth string + Host string + Port int + AllowedDomains string `xorm:"TEXT"` + TLS bool + SkipVerify bool +} + +func (cfg *SMTPConfig) FromDB(bs []byte) error { + return json.Unmarshal(bs, cfg) +} + +func (cfg *SMTPConfig) ToDB() ([]byte, error) { + return json.Marshal(cfg) +} + +type PAMConfig struct { + ServiceName string // pam service (e.g. system-auth) +} + +func (cfg *PAMConfig) FromDB(bs []byte) error { + return json.Unmarshal(bs, &cfg) +} + +func (cfg *PAMConfig) ToDB() ([]byte, error) { + return json.Marshal(cfg) +} + +// LoginSource represents an external way for authorizing users. +type LoginSource struct { + ID int64 `xorm:"pk autoincr"` + Type LoginType + Name string `xorm:"UNIQUE"` + IsActived bool `xorm:"NOT NULL DEFAULT false"` + Cfg core.Conversion `xorm:"TEXT"` + + Created time.Time `xorm:"-"` + CreatedUnix int64 + Updated time.Time `xorm:"-"` + UpdatedUnix int64 +} + +func (s *LoginSource) BeforeInsert() { + s.CreatedUnix = time.Now().Unix() + s.UpdatedUnix = s.CreatedUnix +} + +func (s *LoginSource) BeforeUpdate() { + s.UpdatedUnix = time.Now().Unix() +} + +// Cell2Int64 converts a xorm.Cell type to int64, +// and handles possible irregular cases. +func Cell2Int64(val xorm.Cell) int64 { + switch (*val).(type) { + case []uint8: + log.Trace("Cell2Int64 ([]uint8): %v", *val) + return com.StrTo(string((*val).([]uint8))).MustInt64() + } + return (*val).(int64) +} + +func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) { + switch colName { + case "type": + switch LoginType(Cell2Int64(val)) { + case LOGIN_LDAP, LOGIN_DLDAP: + source.Cfg = new(LDAPConfig) + case LOGIN_SMTP: + source.Cfg = new(SMTPConfig) + case LOGIN_PAM: + source.Cfg = new(PAMConfig) + default: + panic("unrecognized login source type: " + com.ToStr(*val)) + } + } +} + +func (s *LoginSource) AfterSet(colName string, _ xorm.Cell) { + switch colName { + case "created_unix": + s.Created = time.Unix(s.CreatedUnix, 0).Local() + case "updated_unix": + s.Updated = time.Unix(s.UpdatedUnix, 0).Local() + } +} + +func (source *LoginSource) TypeName() string { + return LoginNames[source.Type] +} + +func (source *LoginSource) IsLDAP() bool { + return source.Type == LOGIN_LDAP +} + +func (source *LoginSource) IsDLDAP() bool { + return source.Type == LOGIN_DLDAP +} + +func (source *LoginSource) IsSMTP() bool { + return source.Type == LOGIN_SMTP +} + +func (source *LoginSource) IsPAM() bool { + return source.Type == LOGIN_PAM +} + +func (source *LoginSource) HasTLS() bool { + return ((source.IsLDAP() || source.IsDLDAP()) && + source.LDAP().SecurityProtocol > ldap.SECURITY_PROTOCOL_UNENCRYPTED) || + source.IsSMTP() +} + +func (source *LoginSource) UseTLS() bool { + switch source.Type { + case LOGIN_LDAP, LOGIN_DLDAP: + return source.LDAP().SecurityProtocol != ldap.SECURITY_PROTOCOL_UNENCRYPTED + case LOGIN_SMTP: + return source.SMTP().TLS + } + + return false +} + +func (source *LoginSource) SkipVerify() bool { + switch source.Type { + case LOGIN_LDAP, LOGIN_DLDAP: + return source.LDAP().SkipVerify + case LOGIN_SMTP: + return source.SMTP().SkipVerify + } + + return false +} + +func (source *LoginSource) LDAP() *LDAPConfig { + return source.Cfg.(*LDAPConfig) +} + +func (source *LoginSource) SMTP() *SMTPConfig { + return source.Cfg.(*SMTPConfig) +} + +func (source *LoginSource) PAM() *PAMConfig { + return source.Cfg.(*PAMConfig) +} +func CreateLoginSource(source *LoginSource) error { + has, err := x.Get(&LoginSource{Name: source.Name}) + if err != nil { + return err + } else if has { + return ErrLoginSourceAlreadyExist{source.Name} + } + + _, err = x.Insert(source) + return err +} + +func LoginSources() ([]*LoginSource, error) { + auths := make([]*LoginSource, 0, 5) + return auths, x.Find(&auths) +} + +// GetLoginSourceByID returns login source by given ID. +func GetLoginSourceByID(id int64) (*LoginSource, error) { + source := new(LoginSource) + has, err := x.Id(id).Get(source) + if err != nil { + return nil, err + } else if !has { + return nil, ErrLoginSourceNotExist{id} + } + return source, nil +} + +func UpdateSource(source *LoginSource) error { + _, err := x.Id(source.ID).AllCols().Update(source) + return err +} + +func DeleteSource(source *LoginSource) error { + count, err := x.Count(&User{LoginSource: source.ID}) + if err != nil { + return err + } else if count > 0 { + return ErrLoginSourceInUse{source.ID} + } + _, err = x.Id(source.ID).Delete(new(LoginSource)) + return err +} + +// CountLoginSources returns number of login sources. +func CountLoginSources() int64 { + count, _ := x.Count(new(LoginSource)) + return count +} + +// .____ ________ _____ __________ +// | | \______ \ / _ \\______ \ +// | | | | \ / /_\ \| ___/ +// | |___ | ` \/ | \ | +// |_______ \/_______ /\____|__ /____| +// \/ \/ \/ + +func composeFullName(firstname, surname, username string) string { + switch { + case len(firstname) == 0 && len(surname) == 0: + return username + case len(firstname) == 0: + return surname + case len(surname) == 0: + return firstname + default: + return firstname + " " + surname + } +} + +// LoginViaLDAP queries if login/password is valid against the LDAP directory pool, +// and create a local user if success when enabled. +func LoginViaLDAP(user *User, login, passowrd string, source *LoginSource, autoRegister bool) (*User, error) { + username, fn, sn, mail, isAdmin, succeed := source.Cfg.(*LDAPConfig).SearchEntry(login, passowrd, source.Type == LOGIN_DLDAP) + if !succeed { + // User not in LDAP, do nothing + return nil, ErrUserNotExist{0, login} + } + + if !autoRegister { + return user, nil + } + + // Fallback. + if len(username) == 0 { + username = login + } + // Validate username make sure it satisfies requirement. + if binding.AlphaDashDotPattern.MatchString(username) { + return nil, fmt.Errorf("Invalid pattern for attribute 'username' [%s]: must be valid alpha or numeric or dash(-_) or dot characters", username) + } + + if len(mail) == 0 { + mail = fmt.Sprintf("%s@localhost", username) + } + + user = &User{ + LowerName: strings.ToLower(username), + Name: username, + FullName: composeFullName(fn, sn, username), + Email: mail, + LoginType: source.Type, + LoginSource: source.ID, + LoginName: login, + IsActive: true, + IsAdmin: isAdmin, + } + return user, CreateUser(user) +} + +// _________ __________________________ +// / _____/ / \__ ___/\______ \ +// \_____ \ / \ / \| | | ___/ +// / \/ Y \ | | | +// /_______ /\____|__ /____| |____| +// \/ \/ + +type smtpLoginAuth struct { + username, password string +} + +func (auth *smtpLoginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { + return "LOGIN", []byte(auth.username), nil +} + +func (auth *smtpLoginAuth) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + switch string(fromServer) { + case "Username:": + return []byte(auth.username), nil + case "Password:": + return []byte(auth.password), nil + } + } + return nil, nil +} + +const ( + SMTP_PLAIN = "PLAIN" + SMTP_LOGIN = "LOGIN" +) + +var SMTPAuths = []string{SMTP_PLAIN, SMTP_LOGIN} + +func SMTPAuth(a smtp.Auth, cfg *SMTPConfig) error { + c, err := smtp.Dial(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)) + if err != nil { + return err + } + defer c.Close() + + if err = c.Hello("gogs"); err != nil { + return err + } + + if cfg.TLS { + if ok, _ := c.Extension("STARTTLS"); ok { + if err = c.StartTLS(&tls.Config{ + InsecureSkipVerify: cfg.SkipVerify, + ServerName: cfg.Host, + }); err != nil { + return err + } + } else { + return errors.New("SMTP server unsupports TLS") + } + } + + if ok, _ := c.Extension("AUTH"); ok { + if err = c.Auth(a); err != nil { + return err + } + return nil + } + return ErrUnsupportedLoginType +} + +// LoginViaSMTP queries if login/password is valid against the SMTP, +// and create a local user if success when enabled. +func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPConfig, autoRegister bool) (*User, error) { + // Verify allowed domains. + if len(cfg.AllowedDomains) > 0 { + idx := strings.Index(login, "@") + if idx == -1 { + return nil, ErrUserNotExist{0, login} + } else if !com.IsSliceContainsStr(strings.Split(cfg.AllowedDomains, ","), login[idx+1:]) { + return nil, ErrUserNotExist{0, login} + } + } + + var auth smtp.Auth + if cfg.Auth == SMTP_PLAIN { + auth = smtp.PlainAuth("", login, password, cfg.Host) + } else if cfg.Auth == SMTP_LOGIN { + auth = &smtpLoginAuth{login, password} + } else { + return nil, errors.New("Unsupported SMTP auth type") + } + + if err := SMTPAuth(auth, cfg); err != nil { + // Check standard error format first, + // then fallback to worse case. + tperr, ok := err.(*textproto.Error) + if (ok && tperr.Code == 535) || + strings.Contains(err.Error(), "Username and Password not accepted") { + return nil, ErrUserNotExist{0, login} + } + return nil, err + } + + if !autoRegister { + return user, nil + } + + username := login + idx := strings.Index(login, "@") + if idx > -1 { + username = login[:idx] + } + + user = &User{ + LowerName: strings.ToLower(username), + Name: strings.ToLower(username), + Email: login, + Passwd: password, + LoginType: LOGIN_SMTP, + LoginSource: sourceID, + LoginName: login, + IsActive: true, + } + return user, CreateUser(user) +} + +// __________ _____ _____ +// \______ \/ _ \ / \ +// | ___/ /_\ \ / \ / \ +// | | / | \/ Y \ +// |____| \____|__ /\____|__ / +// \/ \/ + +// LoginViaPAM queries if login/password is valid against the PAM, +// and create a local user if success when enabled. +func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMConfig, autoRegister bool) (*User, error) { + if err := pam.PAMAuth(cfg.ServiceName, login, password); err != nil { + if strings.Contains(err.Error(), "Authentication failure") { + return nil, ErrUserNotExist{0, login} + } + return nil, err + } + + if !autoRegister { + return user, nil + } + + user = &User{ + LowerName: strings.ToLower(login), + Name: login, + Email: login, + Passwd: password, + LoginType: LOGIN_PAM, + LoginSource: sourceID, + LoginName: login, + IsActive: true, + } + return user, CreateUser(user) +} + +func ExternalUserLogin(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) { + if !source.IsActived { + return nil, ErrLoginSourceNotActived + } + + switch source.Type { + case LOGIN_LDAP, LOGIN_DLDAP: + return LoginViaLDAP(user, login, password, source, autoRegister) + case LOGIN_SMTP: + return LoginViaSMTP(user, login, password, source.ID, source.Cfg.(*SMTPConfig), autoRegister) + case LOGIN_PAM: + return LoginViaPAM(user, login, password, source.ID, source.Cfg.(*PAMConfig), autoRegister) + } + + return nil, ErrUnsupportedLoginType +} + +// UserSignIn validates user name and password. +func UserSignIn(username, passowrd string) (*User, error) { + var user *User + if strings.Contains(username, "@") { + user = &User{Email: strings.ToLower(username)} + } else { + user = &User{LowerName: strings.ToLower(username)} + } + + hasUser, err := x.Get(user) + if err != nil { + return nil, err + } + + if hasUser { + switch user.LoginType { + case LOGIN_NOTYPE, LOGIN_PLAIN: + if user.ValidatePassword(passowrd) { + return user, nil + } + + return nil, ErrUserNotExist{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, passowrd, &source, false) + } + } + + sources := make([]*LoginSource, 3) + if err = x.UseBool().Find(&sources, &LoginSource{IsActived: true}); err != nil { + return nil, err + } + + for _, source := range sources { + authUser, err := ExternalUserLogin(nil, username, passowrd, source, true) + if err == nil { + return authUser, nil + } + + log.Warn("Failed to login '%s' via '%s': %v", username, source.Name, err) + } + + return nil, ErrUserNotExist{user.ID, user.Name} +} diff --git a/routers/admin/auths.go b/routers/admin/auths.go index 8d3304eacf..df2512aa06 100644 --- a/routers/admin/auths.go +++ b/routers/admin/auths.go @@ -242,10 +242,9 @@ func DeleteAuthSource(ctx *context.Context) { } if err = models.DeleteSource(source); err != nil { - switch err { - case models.ErrAuthenticationUserUsed: + if models.IsErrLoginSourceInUse(err) { ctx.Flash.Error(ctx.Tr("admin.auths.still_in_used")) - default: + } else { ctx.Flash.Error(fmt.Sprintf("DeleteSource: %v", err)) } ctx.JSON(200, map[string]interface{}{ diff --git a/templates/.VERSION b/templates/.VERSION index 93b1bf2a43..60a098b413 100644 --- a/templates/.VERSION +++ b/templates/.VERSION @@ -1 +1 @@ -0.9.96.0830 \ No newline at end of file +0.9.97.0830 \ No newline at end of file