diff options
author | ᴜɴᴋɴᴡᴏɴ <u@gogs.io> | 2020-09-20 11:19:02 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-09-20 11:19:02 +0800 |
commit | 3af91d7cfdb334e602d312743a89e64cd2d369ee (patch) | |
tree | c04a148917cdd9be878ca0e5fbcd552825c18df7 /internal/db | |
parent | b836a56e6e823eecbce2dd99121a340418f1d5b7 (diff) |
auth: decouple types and functions from db (#6320)
Diffstat (limited to 'internal/db')
-rw-r--r-- | internal/db/backup_test.go | 15 | ||||
-rw-r--r-- | internal/db/errors/login_source.go | 33 | ||||
-rw-r--r-- | internal/db/login_source.go | 342 | ||||
-rw-r--r-- | internal/db/login_source_files.go | 60 | ||||
-rw-r--r-- | internal/db/login_sources.go | 135 | ||||
-rw-r--r-- | internal/db/login_sources_test.go | 63 | ||||
-rw-r--r-- | internal/db/mocks.go | 6 | ||||
-rw-r--r-- | internal/db/testdata/backup/LoginSource.golden.json | 4 | ||||
-rw-r--r-- | internal/db/users.go | 94 | ||||
-rw-r--r-- | internal/db/users_test.go | 51 |
10 files changed, 243 insertions, 560 deletions
diff --git a/internal/db/backup_test.go b/internal/db/backup_test.go index a65e6944..27688889 100644 --- a/internal/db/backup_test.go +++ b/internal/db/backup_test.go @@ -14,6 +14,9 @@ import ( "github.com/pkg/errors" "gorm.io/gorm" + "gogs.io/gogs/internal/auth" + "gogs.io/gogs/internal/auth/github" + "gogs.io/gogs/internal/auth/pam" "gogs.io/gogs/internal/cryptoutil" "gogs.io/gogs/internal/lfsutil" "gogs.io/gogs/internal/testutil" @@ -79,22 +82,22 @@ func setupDBToDump(t *testing.T, db *gorm.DB) { }, &LoginSource{ - Type: LoginPAM, + Type: auth.PAM, Name: "My PAM", IsActived: true, - Config: &PAMConfig{ + Provider: pam.NewProvider(&pam.Config{ ServiceName: "PAM service", - }, + }), CreatedUnix: 1588568886, UpdatedUnix: 1588572486, // 1 hour later }, &LoginSource{ - Type: LoginGitHub, + Type: auth.GitHub, Name: "GitHub.com", IsActived: true, - Config: &GitHubConfig{ + Provider: github.NewProvider(&github.Config{ APIEndpoint: "https://api.github.com", - }, + }), CreatedUnix: 1588568886, }, } diff --git a/internal/db/errors/login_source.go b/internal/db/errors/login_source.go deleted file mode 100644 index db0cd1f9..00000000 --- a/internal/db/errors/login_source.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2017 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 errors - -import "fmt" - -type LoginSourceNotActivated struct { - SourceID int64 -} - -func IsLoginSourceNotActivated(err error) bool { - _, ok := err.(LoginSourceNotActivated) - return ok -} - -func (err LoginSourceNotActivated) Error() string { - return fmt.Sprintf("login source is not activated [source_id: %d]", err.SourceID) -} - -type InvalidLoginSourceType struct { - Type interface{} -} - -func IsInvalidLoginSourceType(err error) bool { - _, ok := err.(InvalidLoginSourceType) - return ok -} - -func (err InvalidLoginSourceType) Error() string { - return fmt.Sprintf("invalid login source type [type: %v]", err.Type) -} diff --git a/internal/db/login_source.go b/internal/db/login_source.go deleted file mode 100644 index 125d771a..00000000 --- a/internal/db/login_source.go +++ /dev/null @@ -1,342 +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. - -// FIXME: Put this file into its own package and separate into different files based on login sources. -package db - -import ( - "crypto/tls" - "fmt" - "net/smtp" - "net/textproto" - "strings" - - "github.com/go-macaron/binding" - "github.com/unknwon/com" - - "gogs.io/gogs/internal/auth/github" - "gogs.io/gogs/internal/auth/ldap" - "gogs.io/gogs/internal/auth/pam" - "gogs.io/gogs/internal/db/errors" -) - -type LoginType int - -// Note: new type must append to the end of list to maintain compatibility. -// TODO: Move to authutil. -const ( - LoginNotype LoginType = iota - LoginPlain // 1 - LoginLDAP // 2 - LoginSMTP // 3 - LoginPAM // 4 - LoginDLDAP // 5 - LoginGitHub // 6 -) - -var LoginNames = map[LoginType]string{ - LoginLDAP: "LDAP (via BindDN)", - LoginDLDAP: "LDAP (simple auth)", // Via direct bind - LoginSMTP: "SMTP", - LoginPAM: "PAM", - LoginGitHub: "GitHub", -} - -// *********************** -// ----- LDAP config ----- -// *********************** - -type LDAPConfig struct { - ldap.Source `ini:"config"` -} - -var SecurityProtocolNames = map[ldap.SecurityProtocol]string{ - ldap.SecurityProtocolUnencrypted: "Unencrypted", - ldap.SecurityProtocolLDAPS: "LDAPS", - ldap.SecurityProtocolStartTLS: "StartTLS", -} - -func (cfg *LDAPConfig) SecurityProtocolName() string { - return SecurityProtocolNames[cfg.SecurityProtocol] -} - -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(login, password string, source *LoginSource, autoRegister bool) (*User, error) { - username, fn, sn, mail, isAdmin, succeed := source.Config.(*LDAPConfig).SearchEntry(login, password, source.Type == LoginDLDAP) - if !succeed { - // User not in LDAP, do nothing - return nil, ErrUserNotExist{args: map[string]interface{}{"login": login}} - } - - if !autoRegister { - return nil, 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, - LoginSource: source.ID, - LoginName: login, - IsActive: true, - IsAdmin: isAdmin, - } - - ok, err := IsUserExist(0, user.Name) - if err != nil { - return user, err - } - - if ok { - return user, UpdateUser(user) - } - - return user, CreateUser(user) -} - -// *********************** -// ----- SMTP config ----- -// *********************** - -type SMTPConfig struct { - Auth string - Host string - Port int - AllowedDomains string - TLS bool `ini:"tls"` - SkipVerify bool -} - -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 ( - SMTPPlain = "PLAIN" - SMTPLogin = "LOGIN" -) - -var SMTPAuths = []string{SMTPPlain, SMTPLogin} - -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 errors.New("Unsupported SMTP authentication method") -} - -// LoginViaSMTP queries if login/password is valid against the SMTP, -// and create a local user if success when enabled. -func LoginViaSMTP(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{args: map[string]interface{}{"login": login}} - } else if !com.IsSliceContainsStr(strings.Split(cfg.AllowedDomains, ","), login[idx+1:]) { - return nil, ErrUserNotExist{args: map[string]interface{}{"login": login}} - } - } - - var auth smtp.Auth - if cfg.Auth == SMTPPlain { - auth = smtp.PlainAuth("", login, password, cfg.Host) - } else if cfg.Auth == SMTPLogin { - auth = &smtpLoginAuth{login, password} - } else { - return nil, errors.New("Unsupported SMTP authentication 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{args: map[string]interface{}{"login": login}} - } - return nil, err - } - - if !autoRegister { - return nil, 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, - LoginSource: sourceID, - LoginName: login, - IsActive: true, - } - return user, CreateUser(user) -} - -// ********************** -// ----- PAM config ----- -// ********************** - -type PAMConfig struct { - // The name of the PAM service, e.g. system-auth. - ServiceName string -} - -// LoginViaPAM queries if login/password is valid against the PAM, -// and create a local user if success when enabled. -func LoginViaPAM(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{args: map[string]interface{}{"login": login}} - } - return nil, err - } - - if !autoRegister { - return nil, nil - } - - user := &User{ - LowerName: strings.ToLower(login), - Name: login, - Email: login, - Passwd: password, - LoginSource: sourceID, - LoginName: login, - IsActive: true, - } - return user, CreateUser(user) -} - -// ************************* -// ----- GitHub config ----- -// ************************* - -type GitHubConfig struct { - // the GitHub service endpoint, e.g. https://api.github.com/. - APIEndpoint string -} - -func LoginViaGitHub(login, password string, sourceID int64, cfg *GitHubConfig, autoRegister bool) (*User, error) { - fullname, email, url, location, err := github.Authenticate(cfg.APIEndpoint, login, password) - if err != nil { - if strings.Contains(err.Error(), "401") { - return nil, ErrUserNotExist{args: map[string]interface{}{"login": login}} - } - return nil, err - } - - if !autoRegister { - return nil, nil - } - user := &User{ - LowerName: strings.ToLower(login), - Name: login, - FullName: fullname, - Email: email, - Website: url, - Passwd: password, - LoginSource: sourceID, - LoginName: login, - IsActive: true, - Location: location, - } - return user, CreateUser(user) -} - -func authenticateViaLoginSource(source *LoginSource, login, password string, autoRegister bool) (*User, error) { - if !source.IsActived { - return nil, errors.LoginSourceNotActivated{SourceID: source.ID} - } - - switch source.Type { - case LoginLDAP, LoginDLDAP: - return LoginViaLDAP(login, password, source, autoRegister) - case LoginSMTP: - return LoginViaSMTP(login, password, source.ID, source.Config.(*SMTPConfig), autoRegister) - case LoginPAM: - return LoginViaPAM(login, password, source.ID, source.Config.(*PAMConfig), autoRegister) - case LoginGitHub: - return LoginViaGitHub(login, password, source.ID, source.Config.(*GitHubConfig), autoRegister) - } - - return nil, errors.InvalidLoginSourceType{Type: source.Type} -} diff --git a/internal/db/login_source_files.go b/internal/db/login_source_files.go index f2a24ad9..ff11b71a 100644 --- a/internal/db/login_source_files.go +++ b/internal/db/login_source_files.go @@ -15,6 +15,11 @@ import ( "github.com/pkg/errors" "gopkg.in/ini.v1" + "gogs.io/gogs/internal/auth" + "gogs.io/gogs/internal/auth/github" + "gogs.io/gogs/internal/auth/ldap" + "gogs.io/gogs/internal/auth/pam" + "gogs.io/gogs/internal/auth/smtp" "gogs.io/gogs/internal/errutil" "gogs.io/gogs/internal/osutil" ) @@ -154,30 +159,57 @@ func loadLoginSourceFiles(authdPath string, clock func() time.Time) (loginSource // Parse authentication source file authType := s.Key("type").String() + cfgSection := authSource.Section("config") switch authType { case "ldap_bind_dn": - loginSource.Type = LoginLDAP - loginSource.Config = &LDAPConfig{} + var cfg ldap.Config + err = cfgSection.MapTo(&cfg) + if err != nil { + return errors.Wrap(err, `map "config" section`) + } + loginSource.Type = auth.LDAP + loginSource.Provider = ldap.NewProvider(false, &cfg) + case "ldap_simple_auth": - loginSource.Type = LoginDLDAP - loginSource.Config = &LDAPConfig{} + var cfg ldap.Config + err = cfgSection.MapTo(&cfg) + if err != nil { + return errors.Wrap(err, `map "config" section`) + } + loginSource.Type = auth.DLDAP + loginSource.Provider = ldap.NewProvider(true, &cfg) + case "smtp": - loginSource.Type = LoginSMTP - loginSource.Config = &SMTPConfig{} + var cfg smtp.Config + err = cfgSection.MapTo(&cfg) + if err != nil { + return errors.Wrap(err, `map "config" section`) + } + loginSource.Type = auth.SMTP + loginSource.Provider = smtp.NewProvider(&cfg) + case "pam": - loginSource.Type = LoginPAM - loginSource.Config = &PAMConfig{} + var cfg pam.Config + err = cfgSection.MapTo(&cfg) + if err != nil { + return errors.Wrap(err, `map "config" section`) + } + loginSource.Type = auth.PAM + loginSource.Provider = pam.NewProvider(&cfg) + case "github": - loginSource.Type = LoginGitHub - loginSource.Config = &GitHubConfig{} + var cfg github.Config + err = cfgSection.MapTo(&cfg) + if err != nil { + return errors.Wrap(err, `map "config" section`) + } + loginSource.Type = auth.GitHub + loginSource.Provider = github.NewProvider(&cfg) + default: return fmt.Errorf("unknown type %q", authType) } - if err = authSource.Section("config").MapTo(loginSource.Config); err != nil { - return errors.Wrap(err, `map "config" section`) - } - store.sources = append(store.sources, loginSource) return nil }) diff --git a/internal/db/login_sources.go b/internal/db/login_sources.go index 4bb1f4c2..1cce7829 100644 --- a/internal/db/login_sources.go +++ b/internal/db/login_sources.go @@ -13,7 +13,11 @@ import ( "github.com/pkg/errors" "gorm.io/gorm" + "gogs.io/gogs/internal/auth" + "gogs.io/gogs/internal/auth/github" "gogs.io/gogs/internal/auth/ldap" + "gogs.io/gogs/internal/auth/pam" + "gogs.io/gogs/internal/auth/smtp" "gogs.io/gogs/internal/errutil" ) @@ -46,12 +50,12 @@ var LoginSources LoginSourcesStore // LoginSource represents an external way for authorizing users. type LoginSource struct { ID int64 - Type LoginType - Name string `xorm:"UNIQUE" gorm:"UNIQUE"` - IsActived bool `xorm:"NOT NULL DEFAULT false" gorm:"NOT NULL"` - IsDefault bool `xorm:"DEFAULT false"` - Config interface{} `xorm:"-" gorm:"-"` - RawConfig string `xorm:"TEXT cfg" gorm:"COLUMN:cfg;TYPE:TEXT"` + Type auth.Type + Name string `xorm:"UNIQUE" gorm:"UNIQUE"` + IsActived bool `xorm:"NOT NULL DEFAULT false" gorm:"NOT NULL"` + IsDefault bool `xorm:"DEFAULT false"` + Provider auth.Provider `xorm:"-" gorm:"-"` + Config string `xorm:"TEXT cfg" gorm:"COLUMN:cfg;TYPE:TEXT" json:"RawConfig"` Created time.Time `xorm:"-" gorm:"-" json:"-"` CreatedUnix int64 @@ -63,10 +67,10 @@ type LoginSource struct { // NOTE: This is a GORM save hook. func (s *LoginSource) BeforeSave(tx *gorm.DB) (err error) { - if s.Config == nil { + if s.Provider == nil { return nil } - s.RawConfig, err = jsoniter.MarshalToString(s.Config) + s.Config, err = jsoniter.MarshalToString(s.Provider.Config()) return err } @@ -91,86 +95,90 @@ func (s *LoginSource) AfterFind(tx *gorm.DB) error { s.Updated = time.Unix(s.UpdatedUnix, 0).Local() switch s.Type { - case LoginLDAP, LoginDLDAP: - s.Config = new(LDAPConfig) - case LoginSMTP: - s.Config = new(SMTPConfig) - case LoginPAM: - s.Config = new(PAMConfig) - case LoginGitHub: - s.Config = new(GitHubConfig) + case auth.LDAP: + var cfg ldap.Config + err := jsoniter.UnmarshalFromString(s.Config, &cfg) + if err != nil { + return err + } + s.Provider = ldap.NewProvider(false, &cfg) + + case auth.DLDAP: + var cfg ldap.Config + err := jsoniter.UnmarshalFromString(s.Config, &cfg) + if err != nil { + return err + } + s.Provider = ldap.NewProvider(true, &cfg) + + case auth.SMTP: + var cfg smtp.Config + err := jsoniter.UnmarshalFromString(s.Config, &cfg) + if err != nil { + return err + } + s.Provider = smtp.NewProvider(&cfg) + + case auth.PAM: + var cfg pam.Config + err := jsoniter.UnmarshalFromString(s.Config, &cfg) + if err != nil { + return err + } + s.Provider = pam.NewProvider(&cfg) + + case auth.GitHub: + var cfg github.Config + err := jsoniter.UnmarshalFromString(s.Config, &cfg) + if err != nil { + return err + } + s.Provider = github.NewProvider(&cfg) + default: return fmt.Errorf("unrecognized login source type: %v", s.Type) } - return jsoniter.UnmarshalFromString(s.RawConfig, s.Config) + return nil } func (s *LoginSource) TypeName() string { - return LoginNames[s.Type] + return auth.Name(s.Type) } func (s *LoginSource) IsLDAP() bool { - return s.Type == LoginLDAP + return s.Type == auth.LDAP } func (s *LoginSource) IsDLDAP() bool { - return s.Type == LoginDLDAP + return s.Type == auth.DLDAP } func (s *LoginSource) IsSMTP() bool { - return s.Type == LoginSMTP + return s.Type == auth.SMTP } func (s *LoginSource) IsPAM() bool { - return s.Type == LoginPAM + return s.Type == auth.PAM } func (s *LoginSource) IsGitHub() bool { - return s.Type == LoginGitHub -} - -func (s *LoginSource) HasTLS() bool { - return ((s.IsLDAP() || s.IsDLDAP()) && - s.LDAP().SecurityProtocol > ldap.SecurityProtocolUnencrypted) || - s.IsSMTP() + return s.Type == auth.GitHub } -func (s *LoginSource) UseTLS() bool { - switch s.Type { - case LoginLDAP, LoginDLDAP: - return s.LDAP().SecurityProtocol != ldap.SecurityProtocolUnencrypted - case LoginSMTP: - return s.SMTP().TLS - } - - return false +func (s *LoginSource) LDAP() *ldap.Config { + return s.Provider.Config().(*ldap.Config) } -func (s *LoginSource) SkipVerify() bool { - switch s.Type { - case LoginLDAP, LoginDLDAP: - return s.LDAP().SkipVerify - case LoginSMTP: - return s.SMTP().SkipVerify - } - - return false +func (s *LoginSource) SMTP() *smtp.Config { + return s.Provider.Config().(*smtp.Config) } -func (s *LoginSource) LDAP() *LDAPConfig { - return s.Config.(*LDAPConfig) +func (s *LoginSource) PAM() *pam.Config { + return s.Provider.Config().(*pam.Config) } -func (s *LoginSource) SMTP() *SMTPConfig { - return s.Config.(*SMTPConfig) -} - -func (s *LoginSource) PAM() *PAMConfig { - return s.Config.(*PAMConfig) -} - -func (s *LoginSource) GitHub() *GitHubConfig { - return s.Config.(*GitHubConfig) +func (s *LoginSource) GitHub() *github.Config { + return s.Provider.Config().(*github.Config) } var _ LoginSourcesStore = (*loginSources)(nil) @@ -181,7 +189,7 @@ type loginSources struct { } type CreateLoginSourceOpts struct { - Type LoginType + Type auth.Type Name string Activated bool Default bool @@ -214,7 +222,10 @@ func (db *loginSources) Create(opts CreateLoginSourceOpts) (*LoginSource, error) Name: opts.Name, IsActived: opts.Activated, IsDefault: opts.Default, - Config: opts.Config, + } + source.Config, err = jsoniter.MarshalToString(opts.Config) + if err != nil { + return nil, err } return source, db.DB.Create(source).Error } @@ -308,7 +319,7 @@ func (db *loginSources) Save(source *LoginSource) error { source.File.SetGeneral("name", source.Name) source.File.SetGeneral("is_activated", strconv.FormatBool(source.IsActived)) source.File.SetGeneral("is_default", strconv.FormatBool(source.IsDefault)) - if err := source.File.SetConfig(source.Config); err != nil { + if err := source.File.SetConfig(source.Provider.Config()); err != nil { return errors.Wrap(err, "set config") } else if err = source.File.Save(); err != nil { return errors.Wrap(err, "save file") diff --git a/internal/db/login_sources_test.go b/internal/db/login_sources_test.go index cb3a16e7..280a1bca 100644 --- a/internal/db/login_sources_test.go +++ b/internal/db/login_sources_test.go @@ -11,6 +11,9 @@ import ( "github.com/stretchr/testify/assert" "gorm.io/gorm" + "gogs.io/gogs/internal/auth" + "gogs.io/gogs/internal/auth/github" + "gogs.io/gogs/internal/auth/pam" "gogs.io/gogs/internal/errutil" ) @@ -30,18 +33,20 @@ func TestLoginSource_BeforeSave(t *testing.T) { if err != nil { t.Fatal(err) } - assert.Empty(t, s.RawConfig) + assert.Empty(t, s.Config) }) t.Run("Config has been set", func(t *testing.T) { s := &LoginSource{ - Config: &PAMConfig{ServiceName: "pam_service"}, + Provider: pam.NewProvider(&pam.Config{ + ServiceName: "pam_service", + }), } err := s.BeforeSave(db) if err != nil { t.Fatal(err) } - assert.Equal(t, `{"ServiceName":"pam_service"}`, s.RawConfig) + assert.Equal(t, `{"ServiceName":"pam_service"}`, s.Config) }) } @@ -109,11 +114,11 @@ func Test_loginSources(t *testing.T) { func test_loginSources_Create(t *testing.T, db *loginSources) { // Create first login source with name "GitHub" source, err := db.Create(CreateLoginSourceOpts{ - Type: LoginGitHub, + Type: auth.GitHub, Name: "GitHub", Activated: true, Default: false, - Config: &GitHubConfig{ + Config: &github.Config{ APIEndpoint: "https://api.github.com", }, }) @@ -138,11 +143,11 @@ func test_loginSources_Create(t *testing.T, db *loginSources) { func test_loginSources_Count(t *testing.T, db *loginSources) { // Create two login sources, one in database and one as source file. _, err := db.Create(CreateLoginSourceOpts{ - Type: LoginGitHub, + Type: auth.GitHub, Name: "GitHub", Activated: true, Default: false, - Config: &GitHubConfig{ + Config: &github.Config{ APIEndpoint: "https://api.github.com", }, }) @@ -162,11 +167,11 @@ func test_loginSources_Count(t *testing.T, db *loginSources) { func test_loginSources_DeleteByID(t *testing.T, db *loginSources) { t.Run("delete but in used", func(t *testing.T) { source, err := db.Create(CreateLoginSourceOpts{ - Type: LoginGitHub, + Type: auth.GitHub, Name: "GitHub", Activated: true, Default: false, - Config: &GitHubConfig{ + Config: &github.Config{ APIEndpoint: "https://api.github.com", }, }) @@ -175,8 +180,7 @@ func test_loginSources_DeleteByID(t *testing.T, db *loginSources) { } // Create a user that uses this login source - _, err = (&users{DB: db.DB}).Create(CreateUserOpts{ - Name: "alice", + _, err = (&users{DB: db.DB}).Create("alice", "", CreateUserOpts{ LoginSource: source.ID, }) if err != nil { @@ -197,11 +201,11 @@ func test_loginSources_DeleteByID(t *testing.T, db *loginSources) { // Create a login source with name "GitHub2" source, err := db.Create(CreateLoginSourceOpts{ - Type: LoginGitHub, + Type: auth.GitHub, Name: "GitHub2", Activated: true, Default: false, - Config: &GitHubConfig{ + Config: &github.Config{ APIEndpoint: "https://api.github.com", }, }) @@ -243,13 +247,13 @@ func test_loginSources_GetByID(t *testing.T, db *loginSources) { }, }) - expConfig := &GitHubConfig{ + expConfig := &github.Config{ APIEndpoint: "https://api.github.com", } // Create a login source with name "GitHub" source, err := db.Create(CreateLoginSourceOpts{ - Type: LoginGitHub, + Type: auth.GitHub, Name: "GitHub", Activated: true, Default: false, @@ -264,7 +268,7 @@ func test_loginSources_GetByID(t *testing.T, db *loginSources) { if err != nil { t.Fatal(err) } - assert.Equal(t, expConfig, source.Config) + assert.Equal(t, expConfig, source.Provider.Config()) // Get the one in source file store _, err = db.GetByID(101) @@ -290,9 +294,9 @@ func test_loginSources_List(t *testing.T, db *loginSources) { // Create two login sources in database, one activated and the other one not _, err := db.Create(CreateLoginSourceOpts{ - Type: LoginPAM, + Type: auth.PAM, Name: "PAM", - Config: &PAMConfig{ + Config: &pam.Config{ ServiceName: "PAM", }, }) @@ -300,10 +304,10 @@ func test_loginSources_List(t *testing.T, db *loginSources) { t.Fatal(err) } _, err = db.Create(CreateLoginSourceOpts{ - Type: LoginGitHub, + Type: auth.GitHub, Name: "GitHub", Activated: true, - Config: &GitHubConfig{ + Config: &github.Config{ APIEndpoint: "https://api.github.com", }, }) @@ -348,10 +352,10 @@ func test_loginSources_ResetNonDefault(t *testing.T, db *loginSources) { // Create two login sources both have default on source1, err := db.Create(CreateLoginSourceOpts{ - Type: LoginPAM, + Type: auth.PAM, Name: "PAM", Default: true, - Config: &PAMConfig{ + Config: &pam.Config{ ServiceName: "PAM", }, }) @@ -359,11 +363,11 @@ func test_loginSources_ResetNonDefault(t *testing.T, db *loginSources) { t.Fatal(err) } source2, err := db.Create(CreateLoginSourceOpts{ - Type: LoginGitHub, + Type: auth.GitHub, Name: "GitHub", Activated: true, Default: true, - Config: &GitHubConfig{ + Config: &github.Config{ APIEndpoint: "https://api.github.com", }, }) @@ -395,11 +399,11 @@ func test_loginSources_Save(t *testing.T, db *loginSources) { t.Run("save to database", func(t *testing.T) { // Create a login source with name "GitHub" source, err := db.Create(CreateLoginSourceOpts{ - Type: LoginGitHub, + Type: auth.GitHub, Name: "GitHub", Activated: true, Default: false, - Config: &GitHubConfig{ + Config: &github.Config{ APIEndpoint: "https://api.github.com", }, }) @@ -408,9 +412,9 @@ func test_loginSources_Save(t *testing.T, db *loginSources) { } source.IsActived = false - source.Config = &GitHubConfig{ + source.Provider = github.NewProvider(&github.Config{ APIEndpoint: "https://api2.github.com", - } + }) err = db.Save(source) if err != nil { t.Fatal(err) @@ -427,6 +431,9 @@ func test_loginSources_Save(t *testing.T, db *loginSources) { t.Run("save to file", func(t *testing.T) { calledSave := false source := &LoginSource{ + Provider: github.NewProvider(&github.Config{ + APIEndpoint: "https://api.github.com", + }), File: &mockLoginSourceFileStore{ MockSetGeneral: func(name, value string) {}, MockSetConfig: func(cfg interface{}) error { return nil }, diff --git a/internal/db/mocks.go b/internal/db/mocks.go index 41bb4072..f7f45cab 100644 --- a/internal/db/mocks.go +++ b/internal/db/mocks.go @@ -209,7 +209,7 @@ var _ UsersStore = (*MockUsersStore)(nil) type MockUsersStore struct { MockAuthenticate func(username, password string, loginSourceID int64) (*User, error) - MockCreate func(opts CreateUserOpts) (*User, error) + MockCreate func(username, email string, opts CreateUserOpts) (*User, error) MockGetByEmail func(email string) (*User, error) MockGetByID func(id int64) (*User, error) MockGetByUsername func(username string) (*User, error) @@ -219,8 +219,8 @@ func (m *MockUsersStore) Authenticate(username, password string, loginSourceID i return m.MockAuthenticate(username, password, loginSourceID) } -func (m *MockUsersStore) Create(opts CreateUserOpts) (*User, error) { - return m.MockCreate(opts) +func (m *MockUsersStore) Create(username, email string, opts CreateUserOpts) (*User, error) { + return m.MockCreate(username, email, opts) } func (m *MockUsersStore) GetByEmail(email string) (*User, error) { diff --git a/internal/db/testdata/backup/LoginSource.golden.json b/internal/db/testdata/backup/LoginSource.golden.json index 08a5d1b6..f7209668 100644 --- a/internal/db/testdata/backup/LoginSource.golden.json +++ b/internal/db/testdata/backup/LoginSource.golden.json @@ -1,2 +1,2 @@ -{"ID":1,"Type":4,"Name":"My PAM","IsActived":true,"IsDefault":false,"Config":null,"RawConfig":"{\"ServiceName\":\"PAM service\"}","CreatedUnix":1588568886,"UpdatedUnix":1588572486} -{"ID":2,"Type":6,"Name":"GitHub.com","IsActived":true,"IsDefault":false,"Config":null,"RawConfig":"{\"APIEndpoint\":\"https://api.github.com\"}","CreatedUnix":1588568886,"UpdatedUnix":0} +{"ID":1,"Type":4,"Name":"My PAM","IsActived":true,"IsDefault":false,"Provider":null,"RawConfig":"{\"ServiceName\":\"PAM service\"}","CreatedUnix":1588568886,"UpdatedUnix":1588572486} +{"ID":2,"Type":6,"Name":"GitHub.com","IsActived":true,"IsDefault":false,"Provider":null,"RawConfig":"{\"APIEndpoint\":\"https://api.github.com\",\"SkipVerify\":false}","CreatedUnix":1588568886,"UpdatedUnix":0} diff --git a/internal/db/users.go b/internal/db/users.go index 64535ea3..5e1baf01 100644 --- a/internal/db/users.go +++ b/internal/db/users.go @@ -9,9 +9,11 @@ import ( "strings" "time" + "github.com/go-macaron/binding" "github.com/pkg/errors" "gorm.io/gorm" + "gogs.io/gogs/internal/auth" "gogs.io/gogs/internal/cryptoutil" "gogs.io/gogs/internal/errutil" ) @@ -32,10 +34,10 @@ type UsersStore interface { // When the "loginSourceID" is positive, it tries to authenticate via given // login source and creates a new user when not yet exists in the database. Authenticate(username, password string, loginSourceID int64) (*User, error) - // Create creates a new user and persist to database. + // Create creates a new user and persists to database. // It returns ErrUserAlreadyExist when a user with same name already exists, // or ErrEmailAlreadyUsed if the email has been used by another user. - Create(opts CreateUserOpts) (*User, error) + Create(username, email string, opts CreateUserOpts) (*User, error) // GetByEmail returns the user (not organization) with given email. // It ignores records with unverified emails and returns ErrUserNotExist when not found. GetByEmail(email string) (*User, error) @@ -93,6 +95,9 @@ func (db *users) Authenticate(login, password string, loginSourceID int64) (*Use return nil, errors.Wrap(err, "get user") } + var authSourceID int64 // The login source ID will be used to authenticate the user + createNewUser := false // Whether to create a new user after successful authentication + // User found in the database if err == nil { // Note: This check is unnecessary but to reduce user confusion at login page @@ -107,44 +112,64 @@ func (db *users) Authenticate(login, password string, loginSourceID int64) (*Use return user, nil } - return nil, ErrUserNotExist{args: map[string]interface{}{"userID": user.ID, "name": user.Name}} + return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login, "userID": user.ID}} } - source, err := LoginSources.GetByID(user.LoginSource) - if err != nil { - return nil, errors.Wrap(err, "get login source") - } + authSourceID = user.LoginSource - _, err = authenticateViaLoginSource(source, login, password, false) - if err != nil { - return nil, errors.Wrap(err, "authenticate via login source") + } else { + // Non-local login source is always greater than 0. + if loginSourceID <= 0 { + return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}} } - return user, nil - } - // Non-local login source is always greater than 0. - if loginSourceID <= 0 { - return nil, ErrUserNotExist{args: map[string]interface{}{"login": login}} + authSourceID = loginSourceID + createNewUser = true } - source, err := LoginSources.GetByID(loginSourceID) + source, err := LoginSources.GetByID(authSourceID) if err != nil { return nil, errors.Wrap(err, "get login source") } - user, err = authenticateViaLoginSource(source, login, password, true) + if !source.IsActived { + return nil, errors.Errorf("login source %d is not activated", source.ID) + } + + extAccount, err := source.Provider.Authenticate(login, password) if err != nil { - return nil, errors.Wrap(err, "authenticate via login source") + return nil, err } - return user, nil + + if !createNewUser { + return user, nil + } + + // Validate username make sure it satisfies requirement. + if binding.AlphaDashDotPattern.MatchString(extAccount.Name) { + return nil, fmt.Errorf("invalid pattern for attribute 'username' [%s]: must be valid alpha or numeric or dash(-_) or dot characters", extAccount.Name) + } + + return Users.Create(extAccount.Name, extAccount.Email, CreateUserOpts{ + FullName: extAccount.FullName, + LoginSource: authSourceID, + LoginName: extAccount.Login, + Location: extAccount.Location, + Website: extAccount.Website, + Activated: true, + Admin: extAccount.Admin, + }) } type CreateUserOpts struct { - Name string - Email string + FullName string Password string LoginSource int64 + LoginName string + Location string + Website string Activated bool + Admin bool } type ErrUserAlreadyExist struct { @@ -181,36 +206,41 @@ func (err ErrEmailAlreadyUsed) Error() string { return fmt.Sprintf("email has been used: %v", err.args) } -func (db *users) Create(opts CreateUserOpts) (*User, error) { - err := isUsernameAllowed(opts.Name) +func (db *users) Create(username, email string, opts CreateUserOpts) (*User, error) { + err := isUsernameAllowed(username) if err != nil { return nil, err } - _, err = db.GetByUsername(opts.Name) + _, err = db.GetByUsername(username) if err == nil { - return nil, ErrUserAlreadyExist{args: errutil.Args{"name": opts.Name}} + return nil, ErrUserAlreadyExist{args: errutil.Args{"name": username}} } else if !IsErrUserNotExist(err) { return nil, err } - _, err = db.GetByEmail(opts.Email) + _, err = db.GetByEmail(email) if err == nil { - return nil, ErrEmailAlreadyUsed{args: errutil.Args{"email": opts.Email}} + return nil, ErrEmailAlreadyUsed{args: errutil.Args{"email": email}} } else if !IsErrUserNotExist(err) { return nil, err } user := &User{ - LowerName: strings.ToLower(opts.Name), - Name: opts.Name, - Email: opts.Email, + LowerName: strings.ToLower(username), + Name: username, + FullName: opts.FullName, + Email: email, Passwd: opts.Password, LoginSource: opts.LoginSource, + LoginName: opts.LoginName, + Location: opts.Location, + Website: opts.Website, MaxRepoCreation: -1, IsActive: opts.Activated, - Avatar: cryptoutil.MD5(opts.Email), - AvatarEmail: opts.Email, + IsAdmin: opts.Admin, + Avatar: cryptoutil.MD5(email), + AvatarEmail: email, } user.Rands, err = GetUserSalt() diff --git a/internal/db/users_test.go b/internal/db/users_test.go index 7ff3b9f5..cb93b036 100644 --- a/internal/db/users_test.go +++ b/internal/db/users_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" + "gogs.io/gogs/internal/auth" "gogs.io/gogs/internal/errutil" ) @@ -51,9 +52,7 @@ func Test_users(t *testing.T) { // along with addressing https://github.com/gogs/gogs/issues/6115. func test_users_Authenticate(t *testing.T, db *users) { password := "pa$$word" - alice, err := db.Create(CreateUserOpts{ - Name: "alice", - Email: "alice@example.com", + alice, err := db.Create("alice", "alice@example.com", CreateUserOpts{ Password: password, }) if err != nil { @@ -62,13 +61,13 @@ func test_users_Authenticate(t *testing.T, db *users) { t.Run("user not found", func(t *testing.T) { _, err := db.Authenticate("bob", password, -1) - expErr := ErrUserNotExist{args: map[string]interface{}{"login": "bob"}} + expErr := auth.ErrBadCredentials{Args: map[string]interface{}{"login": "bob"}} assert.Equal(t, expErr, err) }) t.Run("invalid password", func(t *testing.T) { _, err := db.Authenticate(alice.Name, "bad_password", -1) - expErr := ErrUserNotExist{args: map[string]interface{}{"userID": alice.ID, "name": alice.Name}} + expErr := auth.ErrBadCredentials{Args: map[string]interface{}{"login": alice.Name, "userID": alice.ID}} assert.Equal(t, expErr, err) }) @@ -90,9 +89,7 @@ func test_users_Authenticate(t *testing.T, db *users) { } func test_users_Create(t *testing.T, db *users) { - alice, err := db.Create(CreateUserOpts{ - Name: "alice", - Email: "alice@example.com", + alice, err := db.Create("alice", "alice@example.com", CreateUserOpts{ Activated: true, }) if err != nil { @@ -100,26 +97,19 @@ func test_users_Create(t *testing.T, db *users) { } t.Run("name not allowed", func(t *testing.T) { - _, err := db.Create(CreateUserOpts{ - Name: "-", - }) + _, err := db.Create("-", "", CreateUserOpts{}) expErr := ErrNameNotAllowed{args: errutil.Args{"reason": "reserved", "name": "-"}} assert.Equal(t, expErr, err) }) t.Run("name already exists", func(t *testing.T) { - _, err := db.Create(CreateUserOpts{ - Name: alice.Name, - }) + _, err := db.Create(alice.Name, "", CreateUserOpts{}) expErr := ErrUserAlreadyExist{args: errutil.Args{"name": alice.Name}} assert.Equal(t, expErr, err) }) t.Run("email already exists", func(t *testing.T) { - _, err := db.Create(CreateUserOpts{ - Name: "bob", - Email: alice.Email, - }) + _, err := db.Create("bob", alice.Email, CreateUserOpts{}) expErr := ErrEmailAlreadyUsed{args: errutil.Args{"email": alice.Email}} assert.Equal(t, expErr, err) }) @@ -141,10 +131,7 @@ func test_users_GetByEmail(t *testing.T, db *users) { t.Run("ignore organization", func(t *testing.T) { // TODO: Use Orgs.Create to replace SQL hack when the method is available. - org, err := db.Create(CreateUserOpts{ - Name: "gogs", - Email: "gogs@exmaple.com", - }) + org, err := db.Create("gogs", "gogs@exmaple.com", CreateUserOpts{}) if err != nil { t.Fatal(err) } @@ -160,10 +147,7 @@ func test_users_GetByEmail(t *testing.T, db *users) { }) t.Run("by primary email", func(t *testing.T) { - alice, err := db.Create(CreateUserOpts{ - Name: "alice", - Email: "alice@exmaple.com", - }) + alice, err := db.Create("alice", "alice@exmaple.com", CreateUserOpts{}) if err != nil { t.Fatal(err) } @@ -187,10 +171,7 @@ func test_users_GetByEmail(t *testing.T, db *users) { }) t.Run("by secondary email", func(t *testing.T) { - bob, err := db.Create(CreateUserOpts{ - Name: "bob", - Email: "bob@example.com", - }) + bob, err := db.Create("bob", "bob@example.com", CreateUserOpts{}) if err != nil { t.Fatal(err) } @@ -221,10 +202,7 @@ func test_users_GetByEmail(t *testing.T, db *users) { } func test_users_GetByID(t *testing.T, db *users) { - alice, err := db.Create(CreateUserOpts{ - Name: "alice", - Email: "alice@exmaple.com", - }) + alice, err := db.Create("alice", "alice@exmaple.com", CreateUserOpts{}) if err != nil { t.Fatal(err) } @@ -241,10 +219,7 @@ func test_users_GetByID(t *testing.T, db *users) { } func test_users_GetByUsername(t *testing.T, db *users) { - alice, err := db.Create(CreateUserOpts{ - Name: "alice", - Email: "alice@exmaple.com", - }) + alice, err := db.Create("alice", "alice@exmaple.com", CreateUserOpts{}) if err != nil { t.Fatal(err) } |