Browse Source

auth: support authentication source config file (#3142)

pull/5168/head
Unknwon 7 years ago
parent
commit
f2ecfdc96a
No known key found for this signature in database
GPG Key ID: 25B575AE3213B2B3
  1. 2
      Makefile
  2. 29
      conf/auth.d/ldap_bind_dn.conf.example
  3. 30
      conf/auth.d/ldap_simple_auth.conf.example
  4. 10
      conf/auth.d/pam.conf.example
  5. 16
      conf/auth.d/smtp.conf.example
  6. 4
      conf/locale/locale_en-US.ini
  7. 13
      models/error.go
  8. 27
      models/errors/login_source.go
  9. 339
      models/login_source.go
  10. 4
      pkg/auth/auth.go
  11. 17
      pkg/auth/ldap/ldap.go
  12. 4
      pkg/bindata/bindata.go
  13. 7
      pkg/form/user.go
  14. 62
      routes/admin/auths.go
  15. 3
      routes/api/v1/admin/user.go
  16. 1
      routes/install.go
  17. 12
      routes/org/setting.go
  18. 4
      routes/repo/http.go
  19. 40
      routes/user/auth.go
  20. 4
      routes/user/setting.go
  21. 4
      templates/admin/auth/edit.tmpl
  22. 8
      templates/admin/auth/list.tmpl
  23. 8
      templates/admin/auth/new.tmpl
  24. 18
      templates/user/auth/login.tmpl

2
Makefile

@ -56,7 +56,7 @@ release: build pack
bindata: pkg/bindata/bindata.go bindata: pkg/bindata/bindata.go
pkg/bindata/bindata.go: $(DATA_FILES) 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 less: public/css/gogs.css

29
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
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
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
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

4
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! 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_prompt = Sorry, registration has been disabled. Please contact the site administrator.
disable_register_mail = Sorry, email services are 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 remember_me = Remember Me
forgot_password= Forgot Password forgot_password= Forgot Password
forget_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. team_name_been_taken = Team name has already been taken.
email_been_used = Email address has already been used. email_been_used = Email address has already been used.
username_password_incorrect = Username or password is not correct. 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_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_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. 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.skip_tls_verify = Skip TLS Verify
auths.pam_service_name = PAM Service Name auths.pam_service_name = PAM Service Name
auths.enable_auto_register = Enable Auto Registration auths.enable_auto_register = Enable Auto Registration
auths.tips = Tips
auths.edit = Edit Authentication Setting auths.edit = Edit Authentication Setting
auths.activated = This authentication is activated auths.activated = This authentication is activated
auths.new_success = New authentication '%s' has been added successfully. auths.new_success = New authentication '%s' has been added successfully.

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 { type ErrLoginSourceAlreadyExist struct {
Name string Name string
} }

27
models/errors/login_source.go

@ -6,6 +6,19 @@ package errors
import "fmt" 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 { type LoginSourceNotActivated struct {
SourceID int64 SourceID int64
} }
@ -31,3 +44,17 @@ func IsInvalidLoginSourceType(err error) bool {
func (err InvalidLoginSourceType) Error() string { func (err InvalidLoginSourceType) Error() string {
return fmt.Sprintf("invalid login source type [type: %v]", err.Type) 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)
}

339
models/login_source.go

@ -10,7 +10,10 @@ import (
"fmt" "fmt"
"net/smtp" "net/smtp"
"net/textproto" "net/textproto"
"os"
"path"
"strings" "strings"
"sync"
"time" "time"
"github.com/Unknwon/com" "github.com/Unknwon/com"
@ -18,10 +21,12 @@ import (
"github.com/go-xorm/core" "github.com/go-xorm/core"
"github.com/go-xorm/xorm" "github.com/go-xorm/xorm"
log "gopkg.in/clog.v1" log "gopkg.in/clog.v1"
"gopkg.in/ini.v1"
"github.com/gogits/gogs/models/errors" "github.com/gogits/gogs/models/errors"
"github.com/gogits/gogs/pkg/auth/ldap" "github.com/gogits/gogs/pkg/auth/ldap"
"github.com/gogits/gogs/pkg/auth/pam" "github.com/gogits/gogs/pkg/auth/pam"
"github.com/gogits/gogs/pkg/setting"
) )
type LoginType int type LoginType int
@ -57,7 +62,7 @@ var (
) )
type LDAPConfig struct { type LDAPConfig struct {
*ldap.Source *ldap.Source `ini:"config"`
} }
func (cfg *LDAPConfig) FromDB(bs []byte) error { func (cfg *LDAPConfig) FromDB(bs []byte) error {
@ -77,7 +82,7 @@ type SMTPConfig struct {
Host string Host string
Port int Port int
AllowedDomains string `xorm:"TEXT"` AllowedDomains string `xorm:"TEXT"`
TLS bool TLS bool `ini:"tls"`
SkipVerify bool SkipVerify bool
} }
@ -90,7 +95,7 @@ func (cfg *SMTPConfig) ToDB() ([]byte, error) {
} }
type PAMConfig struct { 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 { func (cfg *PAMConfig) FromDB(bs []byte) error {
@ -101,6 +106,27 @@ func (cfg *PAMConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg) 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. // LoginSource represents an external way for authorizing users.
type LoginSource struct { type LoginSource struct {
ID int64 ID int64
@ -113,6 +139,8 @@ type LoginSource struct {
CreatedUnix int64 CreatedUnix int64
Updated time.Time `xorm:"-"` Updated time.Time `xorm:"-"`
UpdatedUnix int64 UpdatedUnix int64
LocalFile *AuthSourceFile `xorm:"-"`
} }
func (s *LoginSource) BeforeInsert() { func (s *LoginSource) BeforeInsert() {
@ -135,16 +163,16 @@ func Cell2Int64(val xorm.Cell) int64 {
return (*val).(int64) return (*val).(int64)
} }
func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) { func (s *LoginSource) BeforeSet(colName string, val xorm.Cell) {
switch colName { switch colName {
case "type": case "type":
switch LoginType(Cell2Int64(val)) { switch LoginType(Cell2Int64(val)) {
case LOGIN_LDAP, LOGIN_DLDAP: case LOGIN_LDAP, LOGIN_DLDAP:
source.Cfg = new(LDAPConfig) s.Cfg = new(LDAPConfig)
case LOGIN_SMTP: case LOGIN_SMTP:
source.Cfg = new(SMTPConfig) s.Cfg = new(SMTPConfig)
case LOGIN_PAM: case LOGIN_PAM:
source.Cfg = new(PAMConfig) s.Cfg = new(PAMConfig)
default: default:
panic("unrecognized login source type: " + com.ToStr(*val)) 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 { func (s *LoginSource) TypeName() string {
return LoginNames[source.Type] return LoginNames[s.Type]
} }
func (source *LoginSource) IsLDAP() bool { func (s *LoginSource) IsLDAP() bool {
return source.Type == LOGIN_LDAP return s.Type == LOGIN_LDAP
} }
func (source *LoginSource) IsDLDAP() bool { func (s *LoginSource) IsDLDAP() bool {
return source.Type == LOGIN_DLDAP return s.Type == LOGIN_DLDAP
} }
func (source *LoginSource) IsSMTP() bool { func (s *LoginSource) IsSMTP() bool {
return source.Type == LOGIN_SMTP return s.Type == LOGIN_SMTP
} }
func (source *LoginSource) IsPAM() bool { func (s *LoginSource) IsPAM() bool {
return source.Type == LOGIN_PAM return s.Type == LOGIN_PAM
} }
func (source *LoginSource) HasTLS() bool { func (s *LoginSource) HasTLS() bool {
return ((source.IsLDAP() || source.IsDLDAP()) && return ((s.IsLDAP() || s.IsDLDAP()) &&
source.LDAP().SecurityProtocol > ldap.SECURITY_PROTOCOL_UNENCRYPTED) || s.LDAP().SecurityProtocol > ldap.SECURITY_PROTOCOL_UNENCRYPTED) ||
source.IsSMTP() s.IsSMTP()
} }
func (source *LoginSource) UseTLS() bool { func (s *LoginSource) UseTLS() bool {
switch source.Type { switch s.Type {
case LOGIN_LDAP, LOGIN_DLDAP: case LOGIN_LDAP, LOGIN_DLDAP:
return source.LDAP().SecurityProtocol != ldap.SECURITY_PROTOCOL_UNENCRYPTED return s.LDAP().SecurityProtocol != ldap.SECURITY_PROTOCOL_UNENCRYPTED
case LOGIN_SMTP: case LOGIN_SMTP:
return source.SMTP().TLS return s.SMTP().TLS
} }
return false return false
} }
func (source *LoginSource) SkipVerify() bool { func (s *LoginSource) SkipVerify() bool {
switch source.Type { switch s.Type {
case LOGIN_LDAP, LOGIN_DLDAP: case LOGIN_LDAP, LOGIN_DLDAP:
return source.LDAP().SkipVerify return s.LDAP().SkipVerify
case LOGIN_SMTP: case LOGIN_SMTP:
return source.SMTP().SkipVerify return s.SMTP().SkipVerify
} }
return false return false
} }
func (source *LoginSource) LDAP() *LDAPConfig { func (s *LoginSource) LDAP() *LDAPConfig {
return source.Cfg.(*LDAPConfig) return s.Cfg.(*LDAPConfig)
} }
func (source *LoginSource) SMTP() *SMTPConfig { func (s *LoginSource) SMTP() *SMTPConfig {
return source.Cfg.(*SMTPConfig) return s.Cfg.(*SMTPConfig)
} }
func (source *LoginSource) PAM() *PAMConfig { func (s *LoginSource) PAM() *PAMConfig {
return source.Cfg.(*PAMConfig) return s.Cfg.(*PAMConfig)
} }
func CreateLoginSource(source *LoginSource) error { func CreateLoginSource(source *LoginSource) error {
has, err := x.Get(&LoginSource{Name: source.Name}) has, err := x.Get(&LoginSource{Name: source.Name})
if err != nil { if err != nil {
@ -231,9 +260,23 @@ func CreateLoginSource(source *LoginSource) error {
return err return err
} }
// LoginSources returns all login sources defined.
func LoginSources() ([]*LoginSource, error) { func LoginSources() ([]*LoginSource, error) {
auths := make([]*LoginSource, 0, 5) sources := make([]*LoginSource, 0, 2)
return auths, x.Find(&auths) 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. // GetLoginSourceByID returns login source by given ID.
@ -243,14 +286,28 @@ func GetLoginSourceByID(id int64) (*LoginSource, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !has {
return nil, ErrLoginSourceNotExist{id} return localLoginSources.GetLoginSourceByID(id)
} }
return source, nil return source, nil
} }
func UpdateSource(source *LoginSource) error { // UpdateLoginSource updates information of login source to database or local file.
_, err := x.Id(source.ID).AllCols().Update(source) func UpdateLoginSource(source *LoginSource) error {
return err 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 { func DeleteSource(source *LoginSource) error {
@ -264,10 +321,151 @@ func DeleteSource(source *LoginSource) error {
return err return err
} }
// CountLoginSources returns number of login sources. // CountLoginSources returns total number of login sources.
func CountLoginSources() int64 { func CountLoginSources() int64 {
count, _ := x.Count(new(LoginSource)) 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) 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 { if !source.IsActived {
return nil, errors.LoginSourceNotActivated{source.ID} 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} return nil, errors.InvalidLoginSourceType{source.Type}
} }
// UserSignIn validates user name and password. // UserLogin validates user name and password via given login source ID.
func UserSignIn(username, password string) (*User, error) { // 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 var user *User
if strings.Contains(username, "@") { if strings.Contains(username, "@") {
user = &User{Email: strings.ToLower(username)} user = &User{Email: strings.ToLower(username)}
@ -525,44 +724,44 @@ func UserSignIn(username, password string) (*User, error) {
hasUser, err := x.Get(user) hasUser, err := x.Get(user)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("get user record: %v", err)
} }
if hasUser { if hasUser {
switch user.LoginType { // Note: This check is unnecessary but to reduce user confusion at login page
case LOGIN_NOTYPE, LOGIN_PLAIN: // 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) { if user.ValidatePassword(password) {
return user, nil return user, nil
} }
return nil, errors.UserNotExist{user.ID, user.Name} return nil, errors.UserNotExist{user.ID, user.Name}
}
default: // Remote login to the login source the user is associated with
var source LoginSource source, err := GetLoginSourceByID(user.LoginSource)
hasSource, err := x.Id(user.LoginSource).Get(&source) if err != nil {
if err != nil { return nil, err
return nil, err
} else if !hasSource {
return nil, ErrLoginSourceNotExist{user.LoginSource}
}
return ExternalUserLogin(user, user.LoginName, password, &source, false)
} }
}
sources := make([]*LoginSource, 0, 3) return remoteUserLogin(user, user.LoginName, password, source, false)
if err = x.UseBool().Find(&sources, &LoginSource{IsActived: true}); err != nil {
return nil, err
} }
for _, source := range sources { // Non-local login source is always greater than 0
authUser, err := ExternalUserLogin(nil, username, password, source, true) if loginSourceID <= 0 {
if err == nil { return nil, errors.UserNotExist{-1, username}
return authUser, nil }
}
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)
} }

4
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" { if len(auths) == 2 && auths[0] == "Basic" {
uname, passwd, _ := tool.BasicAuthDecode(auths[1]) uname, passwd, _ := tool.BasicAuthDecode(auths[1])
u, err := models.UserSignIn(uname, passwd) u, err := models.UserLogin(uname, passwd, -1)
if err != nil { if err != nil {
if !errors.IsUserNotExist(err) { if !errors.IsUserNotExist(err) {
log.Error(4, "UserSignIn: %v", err) log.Error(4, "UserLogin: %v", err)
} }
return nil, false return nil, false
} }

17
pkg/auth/ldap/ldap.go

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

4
pkg/bindata/bindata.go

File diff suppressed because one or more lines are too long

7
pkg/form/user.go

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

62
routes/admin/auths.go

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

3
routes/api/v1/admin/user.go

@ -10,6 +10,7 @@ import (
api "github.com/gogits/go-gogs-client" api "github.com/gogits/go-gogs-client"
"github.com/gogits/gogs/models" "github.com/gogits/gogs/models"
"github.com/gogits/gogs/models/errors"
"github.com/gogits/gogs/pkg/context" "github.com/gogits/gogs/pkg/context"
"github.com/gogits/gogs/pkg/mailer" "github.com/gogits/gogs/pkg/mailer"
"github.com/gogits/gogs/pkg/setting" "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) source, err := models.GetLoginSourceByID(sourceID)
if err != nil { if err != nil {
if models.IsErrLoginSourceNotExist(err) { if errors.IsLoginSourceNotExist(err) {
c.Error(422, "", err) c.Error(422, "", err)
} else { } else {
c.Error(500, "GetLoginSourceByID", err) c.Error(500, "GetLoginSourceByID", err)

1
routes/install.go

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

12
routes/org/setting.go

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

4
routes/repo/http.go

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

40
routes/user/auth.go

@ -80,12 +80,12 @@ func isValidRedirect(url string) bool {
} }
func Login(c *context.Context) { 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) isSucceed, err := AutoLogin(c)
if err != nil { if err != nil {
c.Handle(500, "AutoLogin", err) c.ServerError("AutoLogin", err)
return return
} }
@ -106,7 +106,15 @@ func Login(c *context.Context) {
return 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) { 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) { 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() { if c.HasError() {
c.Success(LOGIN) c.Success(LOGIN)
return return
} }
u, err := models.UserSignIn(f.UserName, f.Password) u, err := models.UserLogin(f.UserName, f.Password, f.LoginSource)
if err != nil { 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) c.RenderWithErr(c.Tr("form.username_password_incorrect"), LOGIN, &f)
} else { case errors.LoginSourceMismatch:
c.ServerError("UserSignIn", err) c.FormErr("LoginSource")
c.RenderWithErr(c.Tr("form.auth_source_mismatch"), LOGIN, &f)
default:
c.ServerError("UserLogin", err)
} }
return return
} }

4
routes/user/setting.go

@ -633,11 +633,11 @@ func SettingsDelete(c *context.Context) {
c.PageIs("SettingsDelete") c.PageIs("SettingsDelete")
if c.Req.Method == "POST" { 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) { if errors.IsUserNotExist(err) {
c.RenderWithErr(c.Tr("form.enterred_invalid_password"), SETTINGS_DELETE, nil) c.RenderWithErr(c.Tr("form.enterred_invalid_password"), SETTINGS_DELETE, nil)
} else { } else {
c.ServerError("UserSignIn", err) c.ServerError("UserLogin", err)
} }
return return
} }

4
templates/admin/auth/edit.tmpl

@ -189,7 +189,9 @@
<div class="field"> <div class="field">
<button class="ui green button">{{.i18n.Tr "admin.auths.update"}}</button> <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> </div>
</form> </form>
</div> </div>

8
templates/admin/auth/list.tmpl

@ -29,7 +29,13 @@
<td>{{.TypeName}}</td> <td>{{.TypeName}}</td>
<td><i class="fa fa{{if .IsActived}}-check{{end}}-square-o"></i></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 .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> <td><a href="{{AppSubURL}}/admin/auths/{{.ID}}"><i class="fa fa-pencil-square-o"></i></a></td>
</tr> </tr>
{{end}} {{end}}

8
templates/admin/auth/new.tmpl

@ -188,14 +188,6 @@
</div> </div>
</form> </form>
</div> </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> </div>
</div> </div>

18
templates/user/auth/login.tmpl

@ -17,6 +17,24 @@
<label for="password">{{.i18n.Tr "password"}}</label> <label for="password">{{.i18n.Tr "password"}}</label>
<input id="password" name="password" type="password" autocomplete="off" value="{{.password}}" required> <input id="password" name="password" type="password" autocomplete="off" value="{{.password}}" required>
</div> </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"> <div class="inline field">
<label></label> <label></label>
<div class="ui checkbox"> <div class="ui checkbox">

Loading…
Cancel
Save