diff options
Diffstat (limited to 'internal/auth')
-rw-r--r-- | internal/auth/auth.go | 196 | ||||
-rw-r--r-- | internal/auth/github/config.go | 58 | ||||
-rw-r--r-- | internal/auth/github/github.go | 50 | ||||
-rw-r--r-- | internal/auth/github/provider.go | 57 | ||||
-rw-r--r-- | internal/auth/ldap/config.go (renamed from internal/auth/ldap/ldap.go) | 136 | ||||
-rw-r--r-- | internal/auth/ldap/provider.go | 78 | ||||
-rw-r--r-- | internal/auth/pam/config.go | 13 | ||||
-rw-r--r-- | internal/auth/pam/pam.go | 18 | ||||
-rw-r--r-- | internal/auth/pam/pam_stub.go | 4 | ||||
-rw-r--r-- | internal/auth/pam/provider.go | 54 | ||||
-rw-r--r-- | internal/auth/smtp/config.go | 58 | ||||
-rw-r--r-- | internal/auth/smtp/provider.go | 132 |
12 files changed, 603 insertions, 251 deletions
diff --git a/internal/auth/auth.go b/internal/auth/auth.go index fe4ad679..2ae012d3 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -5,144 +5,86 @@ package auth import ( - "strings" + "fmt" - "github.com/go-macaron/session" - gouuid "github.com/satori/go.uuid" - "gopkg.in/macaron.v1" - log "unknwon.dev/clog/v2" + "gogs.io/gogs/internal/errutil" +) - "gogs.io/gogs/internal/conf" - "gogs.io/gogs/internal/db" - "gogs.io/gogs/internal/tool" +type Type int + +// Note: New type must append to the end of list to maintain backward compatibility. +const ( + None Type = iota + Plain // 1 + LDAP // 2 + SMTP // 3 + PAM // 4 + DLDAP // 5 + GitHub // 6 ) -func IsAPIPath(url string) bool { - return strings.HasPrefix(url, "/api/") +// Name returns the human-readable name for given authentication type. +func Name(typ Type) string { + return map[Type]string{ + LDAP: "LDAP (via BindDN)", + DLDAP: "LDAP (simple auth)", // Via direct bind + SMTP: "SMTP", + PAM: "PAM", + GitHub: "GitHub", + }[typ] } -// SignedInID returns the id of signed in user, along with one bool value which indicates whether user uses token -// authentication. -func SignedInID(c *macaron.Context, sess session.Store) (_ int64, isTokenAuth bool) { - if !db.HasEngine { - return 0, false - } - - // Check access token. - if IsAPIPath(c.Req.URL.Path) { - tokenSHA := c.Query("token") - if len(tokenSHA) <= 0 { - tokenSHA = c.Query("access_token") - } - if len(tokenSHA) == 0 { - // Well, check with header again. - auHead := c.Req.Header.Get("Authorization") - if len(auHead) > 0 { - auths := strings.Fields(auHead) - if len(auths) == 2 && auths[0] == "token" { - tokenSHA = auths[1] - } - } - } - - // Let's see if token is valid. - if len(tokenSHA) > 0 { - t, err := db.AccessTokens.GetBySHA(tokenSHA) - if err != nil { - if !db.IsErrAccessTokenNotExist(err) { - log.Error("GetAccessTokenBySHA: %v", err) - } - return 0, false - } - if err = db.AccessTokens.Save(t); err != nil { - log.Error("UpdateAccessToken: %v", err) - } - return t.UserID, true - } - } +var _ errutil.NotFound = (*ErrBadCredentials)(nil) - uid := sess.Get("uid") - if uid == nil { - return 0, false - } - if id, ok := uid.(int64); ok { - if _, err := db.GetUserByID(id); err != nil { - if !db.IsErrUserNotExist(err) { - log.Error("Failed to get user by ID: %v", err) - } - return 0, false - } - return id, false - } - return 0, false +type ErrBadCredentials struct { + Args errutil.Args } -// SignedInUser returns the user object of signed in user, along with two bool values, -// which indicate whether user uses HTTP Basic Authentication or token authentication respectively. -func SignedInUser(ctx *macaron.Context, sess session.Store) (_ *db.User, isBasicAuth bool, isTokenAuth bool) { - if !db.HasEngine { - return nil, false, false - } - - uid, isTokenAuth := SignedInID(ctx, sess) - - if uid <= 0 { - if conf.Auth.EnableReverseProxyAuthentication { - webAuthUser := ctx.Req.Header.Get(conf.Auth.ReverseProxyAuthenticationHeader) - if len(webAuthUser) > 0 { - u, err := db.GetUserByName(webAuthUser) - if err != nil { - if !db.IsErrUserNotExist(err) { - log.Error("Failed to get user by name: %v", err) - return nil, false, false - } - - // Check if enabled auto-registration. - if conf.Auth.EnableReverseProxyAutoRegistration { - u := &db.User{ - Name: webAuthUser, - Email: gouuid.NewV4().String() + "@localhost", - Passwd: webAuthUser, - IsActive: true, - } - if err = db.CreateUser(u); err != nil { - // FIXME: should I create a system notice? - log.Error("Failed to create user: %v", err) - return nil, false, false - } else { - return u, false, false - } - } - } - return u, false, false - } - } +func IsErrBadCredentials(err error) bool { + _, ok := err.(ErrBadCredentials) + return ok +} - // Check with basic auth. - baHead := ctx.Req.Header.Get("Authorization") - if len(baHead) > 0 { - auths := strings.Fields(baHead) - if len(auths) == 2 && auths[0] == "Basic" { - uname, passwd, _ := tool.BasicAuthDecode(auths[1]) +func (err ErrBadCredentials) Error() string { + return fmt.Sprintf("bad credentials: %v", err.Args) +} - u, err := db.Users.Authenticate(uname, passwd, -1) - if err != nil { - if !db.IsErrUserNotExist(err) { - log.Error("Failed to authenticate user: %v", err) - } - return nil, false, false - } +func (ErrBadCredentials) NotFound() bool { + return true +} - return u, true, false - } - } - return nil, false, false - } +// ExternalAccount contains queried information returned by an authenticate provider +// for an external account. +type ExternalAccount struct { + // REQUIRED: The login to be used for authenticating against the provider. + Login string + // REQUIRED: The username of the account. + Name string + // The full name of the account. + FullName string + // The email address of the account. + Email string + // The location of the account. + Location string + // The website of the account. + Website string + // Whether the user should be prompted as a site admin. + Admin bool +} - u, err := db.GetUserByID(uid) - if err != nil { - log.Error("GetUserByID: %v", err) - return nil, false, false - } - return u, false, isTokenAuth +// Provider defines an authenticate provider which provides ability to authentication against +// an external identity provider and query external account information. +type Provider interface { + // Authenticate performs authentication against an external identity provider + // using given credentials and returns queried information of the external account. + Authenticate(login, password string) (*ExternalAccount, error) + + // Config returns the underlying configuration of the authenticate provider. + Config() interface{} + // HasTLS returns true if the authenticate provider supports TLS. + HasTLS() bool + // UseTLS returns true if the authenticate provider is configured to use TLS. + UseTLS() bool + // SkipTLSVerify returns true if the authenticate provider is configured to skip TLS verify. + SkipTLSVerify() bool } diff --git a/internal/auth/github/config.go b/internal/auth/github/config.go new file mode 100644 index 00000000..e4636743 --- /dev/null +++ b/internal/auth/github/config.go @@ -0,0 +1,58 @@ +// Copyright 2020 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 github + +import ( + "context" + "crypto/tls" + "net/http" + "strings" + + "github.com/google/go-github/github" + "github.com/pkg/errors" +) + +// Config contains configuration for GitHub authentication. +// +// ⚠️ WARNING: Change to the field name must preserve the INI key name for backward compatibility. +type Config struct { + // the GitHub service endpoint, e.g. https://api.github.com/. + APIEndpoint string + SkipVerify bool +} + +func (c *Config) doAuth(login, password string) (fullname, email, location, website string, err error) { + tp := github.BasicAuthTransport{ + Username: strings.TrimSpace(login), + Password: strings.TrimSpace(password), + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipVerify}, + }, + } + client, err := github.NewEnterpriseClient(c.APIEndpoint, c.APIEndpoint, tp.Client()) + if err != nil { + return "", "", "", "", errors.Wrap(err, "create new client") + } + user, _, err := client.Users.Get(context.Background(), "") + if err != nil { + return "", "", "", "", errors.Wrap(err, "get user info") + } + + if user.Name != nil { + fullname = *user.Name + } + if user.Email != nil { + email = *user.Email + } else { + email = login + "+github@local" + } + if user.Location != nil { + location = strings.ToUpper(*user.Location) + } + if user.HTMLURL != nil { + website = strings.ToLower(*user.HTMLURL) + } + return fullname, email, location, website, nil +} diff --git a/internal/auth/github/github.go b/internal/auth/github/github.go deleted file mode 100644 index a06608a3..00000000 --- a/internal/auth/github/github.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2018 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 github - -import ( - "context" - "crypto/tls" - "fmt" - "net/http" - "strings" - - "github.com/google/go-github/github" -) - -func Authenticate(apiEndpoint, login, passwd string) (name string, email string, website string, location string, _ error) { - tp := github.BasicAuthTransport{ - Username: strings.TrimSpace(login), - Password: strings.TrimSpace(passwd), - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - } - client, err := github.NewEnterpriseClient(apiEndpoint, apiEndpoint, tp.Client()) - if err != nil { - return "", "", "", "", fmt.Errorf("create new client: %v", err) - } - user, _, err := client.Users.Get(context.Background(), "") - if err != nil { - return "", "", "", "", fmt.Errorf("get user info: %v", err) - } - - if user.Name != nil { - name = *user.Name - } - if user.Email != nil { - email = *user.Email - } else { - email = login + "+github@local" - } - if user.HTMLURL != nil { - website = strings.ToLower(*user.HTMLURL) - } - if user.Location != nil { - location = strings.ToUpper(*user.Location) - } - - return name, email, website, location, nil -} diff --git a/internal/auth/github/provider.go b/internal/auth/github/provider.go new file mode 100644 index 00000000..4add2e54 --- /dev/null +++ b/internal/auth/github/provider.go @@ -0,0 +1,57 @@ +// Copyright 2020 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 github + +import ( + "strings" + + "gogs.io/gogs/internal/auth" +) + +// Provider contains configuration of a PAM authentication provider. +type Provider struct { + config *Config +} + +// NewProvider creates a new PAM authentication provider. +func NewProvider(cfg *Config) auth.Provider { + return &Provider{ + config: cfg, + } +} + +func (p *Provider) Authenticate(login, password string) (*auth.ExternalAccount, error) { + fullname, email, website, location, err := p.config.doAuth(login, password) + if err != nil { + if strings.Contains(err.Error(), "401") { + return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}} + } + return nil, err + } + return &auth.ExternalAccount{ + Login: login, + Name: login, + FullName: fullname, + Email: email, + Location: location, + Website: website, + }, nil +} + +func (p *Provider) Config() interface{} { + return p.config +} + +func (p *Provider) HasTLS() bool { + return true +} + +func (p *Provider) UseTLS() bool { + return true +} + +func (p *Provider) SkipTLSVerify() bool { + return p.config.SkipVerify +} diff --git a/internal/auth/ldap/ldap.go b/internal/auth/ldap/config.go index d8f8458e..0c33368d 100644 --- a/internal/auth/ldap/ldap.go +++ b/internal/auth/ldap/config.go @@ -2,8 +2,8 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -// Package ldap provide functions & structure to query a LDAP ldap directory -// For now, it's mainly tested again an MS Active Directory service, see README.md for more information +// Package ldap provide functions & structure to query a LDAP ldap directory. +// For now, it's mainly tested again an MS Active Directory service, see README.md for more information. package ldap import ( @@ -15,19 +15,31 @@ import ( log "unknwon.dev/clog/v2" ) +// SecurityProtocol is the security protocol when the authenticate provider talks to LDAP directory. type SecurityProtocol int -// Note: new type must be added at the end of list to maintain compatibility. +// ⚠️ WARNING: new type must be added at the end of list to maintain compatibility. const ( SecurityProtocolUnencrypted SecurityProtocol = iota SecurityProtocolLDAPS SecurityProtocolStartTLS ) -// Basic LDAP authentication service -type Source struct { +// SecurityProtocolName returns the human-readable name for given security protocol. +func SecurityProtocolName(protocol SecurityProtocol) string { + return map[SecurityProtocol]string{ + SecurityProtocolUnencrypted: "Unencrypted", + SecurityProtocolLDAPS: "LDAPS", + SecurityProtocolStartTLS: "StartTLS", + }[protocol] +} + +// Config contains configuration for LDAP authentication. +// +// ⚠️ WARNING: Change to the field name must preserve the INI key name for backward compatibility. +type Config struct { Host string // LDAP host - Port int // port number + Port int // Port number SecurityProtocol SecurityProtocol SkipVerify bool BindDN string `ini:"bind_dn,omitempty"` // DN to bind with @@ -37,18 +49,22 @@ type Source struct { AttributeUsername string // Username attribute AttributeName string // First name attribute AttributeSurname string // Surname attribute - AttributeMail string // E-mail attribute - AttributesInBind bool // fetch attributes in bind context (not user) + AttributeMail string // Email attribute + AttributesInBind bool // Fetch attributes in bind context (not user) Filter string // Query filter to validate entry AdminFilter string // Query filter to check if user is admin - GroupEnabled bool // if the group checking is enabled - GroupDN string `ini:"group_dn"` // Group Search Base - GroupFilter string // Group Name Filter + GroupEnabled bool // Whether the group checking is enabled + GroupDN string `ini:"group_dn"` // Group search base + GroupFilter string // Group name filter GroupMemberUID string `ini:"group_member_uid"` // Group Attribute containing array of UserUID - UserUID string `ini:"user_uid"` // User Attribute listed in Group + UserUID string `ini:"user_uid"` // User Attribute listed in group +} + +func (c *Config) SecurityProtocolName() string { + return SecurityProtocolName(c.SecurityProtocol) } -func (ls *Source) sanitizedUserQuery(username string) (string, bool) { +func (c *Config) sanitizedUserQuery(username string) (string, bool) { // See http://tools.ietf.org/search/rfc4515 badCharacters := "\x00()*\\" if strings.ContainsAny(username, badCharacters) { @@ -56,10 +72,10 @@ func (ls *Source) sanitizedUserQuery(username string) (string, bool) { return "", false } - return strings.Replace(ls.Filter, "%s", username, -1), true + return strings.Replace(c.Filter, "%s", username, -1), true } -func (ls *Source) sanitizedUserDN(username string) (string, bool) { +func (c *Config) sanitizedUserDN(username string) (string, bool) { // See http://tools.ietf.org/search/rfc4514: "special characters" badCharacters := "\x00()*\\,='\"#+;<>" if strings.ContainsAny(username, badCharacters) || strings.HasPrefix(username, " ") || strings.HasSuffix(username, " ") { @@ -67,10 +83,10 @@ func (ls *Source) sanitizedUserDN(username string) (string, bool) { return "", false } - return strings.Replace(ls.UserDN, "%s", username, -1), true + return strings.Replace(c.UserDN, "%s", username, -1), true } -func (ls *Source) sanitizedGroupFilter(group string) (string, bool) { +func (c *Config) sanitizedGroupFilter(group string) (string, bool) { // See http://tools.ietf.org/search/rfc4515 badCharacters := "\x00*\\" if strings.ContainsAny(group, badCharacters) { @@ -81,7 +97,7 @@ func (ls *Source) sanitizedGroupFilter(group string) (string, bool) { return group, true } -func (ls *Source) sanitizedGroupDN(groupDn string) (string, bool) { +func (c *Config) sanitizedGroupDN(groupDn string) (string, bool) { // See http://tools.ietf.org/search/rfc4514: "special characters" badCharacters := "\x00()*\\'\"#+;<>" if strings.ContainsAny(groupDn, badCharacters) || strings.HasPrefix(groupDn, " ") || strings.HasSuffix(groupDn, " ") { @@ -92,12 +108,12 @@ func (ls *Source) sanitizedGroupDN(groupDn string) (string, bool) { return groupDn, true } -func (ls *Source) findUserDN(l *ldap.Conn, name string) (string, bool) { +func (c *Config) findUserDN(l *ldap.Conn, name string) (string, bool) { log.Trace("Search for LDAP user: %s", name) - if len(ls.BindDN) > 0 && len(ls.BindPassword) > 0 { + if len(c.BindDN) > 0 && len(c.BindPassword) > 0 { // Replace placeholders with username - bindDN := strings.Replace(ls.BindDN, "%s", name, -1) - err := l.Bind(bindDN, ls.BindPassword) + bindDN := strings.Replace(c.BindDN, "%s", name, -1) + err := l.Bind(bindDN, c.BindPassword) if err != nil { log.Trace("LDAP: Failed to bind as BindDN '%s': %v", bindDN, err) return "", false @@ -108,23 +124,23 @@ func (ls *Source) findUserDN(l *ldap.Conn, name string) (string, bool) { } // A search for the user. - userFilter, ok := ls.sanitizedUserQuery(name) + userFilter, ok := c.sanitizedUserQuery(name) if !ok { return "", false } - log.Trace("LDAP: Searching for DN using filter '%s' and base '%s'", userFilter, ls.UserBase) + log.Trace("LDAP: Searching for DN using filter %q and base %q", userFilter, c.UserBase) search := ldap.NewSearchRequest( - ls.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, + c.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter, []string{}, nil) // Ensure we found a user sr, err := l.Search(search) if err != nil || len(sr.Entries) < 1 { - log.Trace("LDAP: Failed search using filter '%s': %v", userFilter, err) + log.Trace("LDAP: Failed to search using filter %q: %v", userFilter, err) return "", false } else if len(sr.Entries) > 1 { - log.Trace("LDAP: Filter '%s' returned more than one user", userFilter) + log.Trace("LDAP: Filter %q returned more than one user", userFilter) return "", false } @@ -137,7 +153,7 @@ func (ls *Source) findUserDN(l *ldap.Conn, name string) (string, bool) { return userDN, true } -func dial(ls *Source) (*ldap.Conn, error) { +func dial(ls *Config) (*ldap.Conn, error) { log.Trace("LDAP: Dialing with security protocol '%v' without verifying: %v", ls.SecurityProtocol, ls.SkipVerify) tlsCfg := &tls.Config{ @@ -174,26 +190,26 @@ func bindUser(l *ldap.Conn, userDN, passwd string) error { return err } -// searchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter -func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, string, string, string, bool, bool) { +// searchEntry searches an LDAP source if an entry (name, passwd) is valid and in the specific filter. +func (c *Config) searchEntry(name, passwd string, directBind bool) (string, string, string, string, bool, bool) { // See https://tools.ietf.org/search/rfc4513#section-5.1.2 if len(passwd) == 0 { log.Trace("authentication failed for '%s' with empty password", name) return "", "", "", "", false, false } - l, err := dial(ls) + l, err := dial(c) if err != nil { - log.Error("LDAP connect failed for '%s': %v", ls.Host, err) + log.Error("LDAP connect failed for '%s': %v", c.Host, err) return "", "", "", "", false, false } defer l.Close() var userDN string if directBind { - log.Trace("LDAP will bind directly via UserDN template: %s", ls.UserDN) + log.Trace("LDAP will bind directly via UserDN template: %s", c.UserDN) var ok bool - userDN, ok = ls.sanitizedUserDN(name) + userDN, ok = c.sanitizedUserDN(name) if !ok { return "", "", "", "", false, false } @@ -201,13 +217,13 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str log.Trace("LDAP will use BindDN") var found bool - userDN, found = ls.findUserDN(l, name) + userDN, found = c.findUserDN(l, name) if !found { return "", "", "", "", false, false } } - if directBind || !ls.AttributesInBind { + if directBind || !c.AttributesInBind { // binds user (checking password) before looking-up attributes in user context err = bindUser(l, userDN, passwd) if err != nil { @@ -215,18 +231,18 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str } } - userFilter, ok := ls.sanitizedUserQuery(name) + userFilter, ok := c.sanitizedUserQuery(name) if !ok { return "", "", "", "", false, false } - log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v' with filter '%s' and base '%s'", - ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.UserUID, userFilter, userDN) + log.Trace("Fetching attributes %q, %q, %q, %q, %q with user filter %q and user DN %q", + c.AttributeUsername, c.AttributeName, c.AttributeSurname, c.AttributeMail, c.UserUID, userFilter, userDN) + search := ldap.NewSearchRequest( userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter, - []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.UserUID}, + []string{c.AttributeUsername, c.AttributeName, c.AttributeSurname, c.AttributeMail, c.UserUID}, nil) - sr, err := l.Search(search) if err != nil { log.Error("LDAP: User search failed: %v", err) @@ -241,27 +257,27 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str return "", "", "", "", false, false } - username := sr.Entries[0].GetAttributeValue(ls.AttributeUsername) - firstname := sr.Entries[0].GetAttributeValue(ls.AttributeName) - surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname) - mail := sr.Entries[0].GetAttributeValue(ls.AttributeMail) - uid := sr.Entries[0].GetAttributeValue(ls.UserUID) + username := sr.Entries[0].GetAttributeValue(c.AttributeUsername) + firstname := sr.Entries[0].GetAttributeValue(c.AttributeName) + surname := sr.Entries[0].GetAttributeValue(c.AttributeSurname) + mail := sr.Entries[0].GetAttributeValue(c.AttributeMail) + uid := sr.Entries[0].GetAttributeValue(c.UserUID) // Check group membership - if ls.GroupEnabled { - groupFilter, ok := ls.sanitizedGroupFilter(ls.GroupFilter) + if c.GroupEnabled { + groupFilter, ok := c.sanitizedGroupFilter(c.GroupFilter) if !ok { return "", "", "", "", false, false } - groupDN, ok := ls.sanitizedGroupDN(ls.GroupDN) + groupDN, ok := c.sanitizedGroupDN(c.GroupDN) if !ok { return "", "", "", "", false, false } - log.Trace("LDAP: Fetching groups '%v' with filter '%s' and base '%s'", ls.GroupMemberUID, groupFilter, groupDN) + log.Trace("LDAP: Fetching groups '%v' with filter '%s' and base '%s'", c.GroupMemberUID, groupFilter, groupDN) groupSearch := ldap.NewSearchRequest( groupDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, groupFilter, - []string{ls.GroupMemberUID}, + []string{c.GroupMemberUID}, nil) srg, err := l.Search(groupSearch) @@ -274,9 +290,9 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str } isMember := false - if ls.UserUID == "dn" { + if c.UserUID == "dn" { for _, group := range srg.Entries { - for _, member := range group.GetAttributeValues(ls.GroupMemberUID) { + for _, member := range group.GetAttributeValues(c.GroupMemberUID) { if member == sr.Entries[0].DN { isMember = true } @@ -284,7 +300,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str } } else { for _, group := range srg.Entries { - for _, member := range group.GetAttributeValues(ls.GroupMemberUID) { + for _, member := range group.GetAttributeValues(c.GroupMemberUID) { if member == uid { isMember = true } @@ -293,17 +309,17 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str } if !isMember { - log.Trace("LDAP: Group membership test failed [username: %s, group_member_uid: %s, user_uid: %s", username, ls.GroupMemberUID, uid) + log.Trace("LDAP: Group membership test failed [username: %s, group_member_uid: %s, user_uid: %s", username, c.GroupMemberUID, uid) return "", "", "", "", false, false } } isAdmin := false - if len(ls.AdminFilter) > 0 { - log.Trace("Checking admin with filter '%s' and base '%s'", ls.AdminFilter, userDN) + if len(c.AdminFilter) > 0 { + log.Trace("Checking admin with filter '%s' and base '%s'", c.AdminFilter, userDN) search = ldap.NewSearchRequest( - userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ls.AdminFilter, - []string{ls.AttributeName}, + userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, c.AdminFilter, + []string{c.AttributeName}, nil) sr, err = l.Search(search) @@ -316,7 +332,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str } } - if !directBind && ls.AttributesInBind { + if !directBind && c.AttributesInBind { // binds user (checking password) after looking-up attributes in BindDN context err = bindUser(l, userDN, passwd) if err != nil { diff --git a/internal/auth/ldap/provider.go b/internal/auth/ldap/provider.go new file mode 100644 index 00000000..a42baa71 --- /dev/null +++ b/internal/auth/ldap/provider.go @@ -0,0 +1,78 @@ +// Copyright 2020 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 ldap + +import ( + "fmt" + + "gogs.io/gogs/internal/auth" +) + +// Provider contains configuration of an LDAP authentication provider. +type Provider struct { + directBind bool + config *Config +} + +// NewProvider creates a new LDAP authentication provider. +func NewProvider(directBind bool, cfg *Config) auth.Provider { + return &Provider{ + directBind: directBind, + config: cfg, + } +} + +// Authenticate queries if login/password is valid against the LDAP directory pool, +// and returns queried information when succeeded. +func (p *Provider) Authenticate(login, password string) (*auth.ExternalAccount, error) { + username, fn, sn, email, isAdmin, succeed := p.config.searchEntry(login, password, p.directBind) + if !succeed { + return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}} + } + + if len(username) == 0 { + username = login + } + if len(email) == 0 { + email = fmt.Sprintf("%s@localhost", username) + } + + composeFullName := func(firstname, surname, username string) string { + switch { + case firstname == "" && surname == "": + return username + case firstname == "": + return surname + case surname == "": + return firstname + default: + return firstname + " " + surname + } + } + + return &auth.ExternalAccount{ + Login: login, + Name: username, + FullName: composeFullName(fn, sn, username), + Email: email, + Admin: isAdmin, + }, nil +} + +func (p *Provider) Config() interface{} { + return p.config +} + +func (p *Provider) HasTLS() bool { + return p.config.SecurityProtocol > SecurityProtocolUnencrypted +} + +func (p *Provider) UseTLS() bool { + return p.config.SecurityProtocol > SecurityProtocolUnencrypted +} + +func (p *Provider) SkipTLSVerify() bool { + return p.config.SkipVerify +} diff --git a/internal/auth/pam/config.go b/internal/auth/pam/config.go new file mode 100644 index 00000000..7a6bc0cc --- /dev/null +++ b/internal/auth/pam/config.go @@ -0,0 +1,13 @@ +// 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 pam + +// Config contains configuration for PAM authentication. +// +// ⚠️ WARNING: Change to the field name must preserve the INI key name for backward compatibility. +type Config struct { + // The name of the PAM service, e.g. system-auth. + ServiceName string +} diff --git a/internal/auth/pam/pam.go b/internal/auth/pam/pam.go index 7f326d42..521cd4f0 100644 --- a/internal/auth/pam/pam.go +++ b/internal/auth/pam/pam.go @@ -7,29 +7,23 @@ package pam import ( - "errors" - "github.com/msteinert/pam" + "github.com/pkg/errors" ) -func PAMAuth(serviceName, userName, passwd string) error { - t, err := pam.StartFunc(serviceName, userName, func(s pam.Style, msg string) (string, error) { +func (c *Config) doAuth(login, password string) error { + t, err := pam.StartFunc(c.ServiceName, login, func(s pam.Style, msg string) (string, error) { switch s { case pam.PromptEchoOff: - return passwd, nil + return password, nil case pam.PromptEchoOn, pam.ErrorMsg, pam.TextInfo: return "", nil } - return "", errors.New("Unrecognized PAM message style") + return "", errors.Errorf("unrecognized PAM message style: %v - %s", s, msg) }) - if err != nil { return err } - if err = t.Authenticate(0); err != nil { - return err - } - - return nil + return t.Authenticate(0) } diff --git a/internal/auth/pam/pam_stub.go b/internal/auth/pam/pam_stub.go index 33ac751a..9392eab5 100644 --- a/internal/auth/pam/pam_stub.go +++ b/internal/auth/pam/pam_stub.go @@ -7,9 +7,9 @@ package pam import ( - "errors" + "github.com/pkg/errors" ) -func PAMAuth(serviceName, userName, passwd string) error { +func (c *Config) doAuth(login, password string) error { return errors.New("PAM not supported") } diff --git a/internal/auth/pam/provider.go b/internal/auth/pam/provider.go new file mode 100644 index 00000000..ad1b7a8c --- /dev/null +++ b/internal/auth/pam/provider.go @@ -0,0 +1,54 @@ +// Copyright 2020 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 pam + +import ( + "strings" + + "gogs.io/gogs/internal/auth" +) + +// Provider contains configuration of a PAM authentication provider. +type Provider struct { + config *Config +} + +// NewProvider creates a new PAM authentication provider. +func NewProvider(cfg *Config) auth.Provider { + return &Provider{ + config: cfg, + } +} + +func (p *Provider) Authenticate(login, password string) (*auth.ExternalAccount, error) { + err := p.config.doAuth(login, password) + if err != nil { + if strings.Contains(err.Error(), "Authentication failure") { + return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}} + } + return nil, err + } + + return &auth.ExternalAccount{ + Login: login, + Name: login, + }, nil +} + +func (p *Provider) Config() interface{} { + return p.config +} + +func (p *Provider) HasTLS() bool { + return false +} + +func (p *Provider) UseTLS() bool { + return false +} + +func (p *Provider) SkipTLSVerify() bool { + return false +} diff --git a/internal/auth/smtp/config.go b/internal/auth/smtp/config.go new file mode 100644 index 00000000..33985f45 --- /dev/null +++ b/internal/auth/smtp/config.go @@ -0,0 +1,58 @@ +// Copyright 2020 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 smtp + +import ( + "crypto/tls" + "fmt" + "net/smtp" + + "github.com/pkg/errors" +) + +// Config contains configuration for SMTP authentication. +// +// ⚠️ WARNING: Change to the field name must preserve the INI key name for backward compatibility. +type Config struct { + Auth string + Host string + Port int + AllowedDomains string + TLS bool `ini:"tls"` + SkipVerify bool +} + +func (c *Config) doAuth(auth smtp.Auth) error { + client, err := smtp.Dial(fmt.Sprintf("%s:%d", c.Host, c.Port)) + if err != nil { + return err + } + defer client.Close() + + if err = client.Hello("gogs"); err != nil { + return err + } + + if c.TLS { + if ok, _ := client.Extension("STARTTLS"); ok { + if err = client.StartTLS(&tls.Config{ + InsecureSkipVerify: c.SkipVerify, + ServerName: c.Host, + }); err != nil { + return err + } + } else { + return errors.New("SMTP server does not support TLS") + } + } + + if ok, _ := client.Extension("AUTH"); ok { + if err = client.Auth(auth); err != nil { + return err + } + return nil + } + return errors.New("unsupported SMTP authentication method") +} diff --git a/internal/auth/smtp/provider.go b/internal/auth/smtp/provider.go new file mode 100644 index 00000000..3e39aa22 --- /dev/null +++ b/internal/auth/smtp/provider.go @@ -0,0 +1,132 @@ +// Copyright 2020 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 smtp + +import ( + "net/smtp" + "net/textproto" + "strings" + + "github.com/pkg/errors" + log "unknwon.dev/clog/v2" + + "gogs.io/gogs/internal/auth" +) + +// Provider contains configuration of an SMTP authentication provider. +type Provider struct { + config *Config +} + +// NewProvider creates a new SMTP authentication provider. +func NewProvider(cfg *Config) auth.Provider { + return &Provider{ + config: cfg, + } +} + +// Authenticate queries if login/password is valid against the SMTP server, +// and returns queried information when succeeded. +func (p *Provider) Authenticate(login, password string) (*auth.ExternalAccount, error) { + // Verify allowed domains + if p.config.AllowedDomains != "" { + fields := strings.SplitN(login, "@", 3) + if len(fields) != 2 { + return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}} + } + domain := fields[1] + + isAllowed := false + for _, allowed := range strings.Split(p.config.AllowedDomains, ",") { + if domain == allowed { + isAllowed = true + break + } + } + + if !isAllowed { + return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}} + } + } + + var smtpAuth smtp.Auth + switch p.config.Auth { + case Plain: + smtpAuth = smtp.PlainAuth("", login, password, p.config.Host) + case Login: + smtpAuth = &smtpLoginAuth{login, password} + default: + return nil, errors.Errorf("unsupported SMTP authentication type %q", p.config.Auth) + } + + if err := p.config.doAuth(smtpAuth); err != nil { + log.Trace("SMTP: Authentication failed: %v", err) + + // Check standard error format first, then fallback to the worse case. + tperr, ok := err.(*textproto.Error) + if (ok && tperr.Code == 535) || + strings.Contains(err.Error(), "Username and Password not accepted") { + return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}} + } + return nil, err + } + + username := login + + // NOTE: It is not required to have "@" in `login` for a successful SMTP authentication. + idx := strings.Index(login, "@") + if idx > -1 { + username = login[:idx] + } + + return &auth.ExternalAccount{ + Login: login, + Name: username, + Email: login, + }, nil +} + +func (p *Provider) Config() interface{} { + return p.config +} + +func (p *Provider) HasTLS() bool { + return true +} + +func (p *Provider) UseTLS() bool { + return p.config.TLS +} + +func (p *Provider) SkipTLSVerify() bool { + return p.config.SkipVerify +} + +const ( + Plain = "PLAIN" + Login = "LOGIN" +) + +var AuthTypes = []string{Plain, Login} + +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 +} |