auth: decouple types and functions from db (#6320)

pull/6342/head
ᴜɴᴋɴᴡᴏɴ 2020-09-20 11:19:02 +08:00 committed by GitHub
parent b836a56e6e
commit 3af91d7cfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1105 additions and 921 deletions

View File

@ -384,7 +384,7 @@ func confAuthDLdap_simple_authConfExample() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "conf/auth.d/ldap_simple_auth.conf.example", size: 761, mode: os.FileMode(0644), modTime: time.Unix(1599160112, 0)}
info := bindataFileInfo{name: "conf/auth.d/ldap_simple_auth.conf.example", size: 761, mode: os.FileMode(0644), modTime: time.Unix(1600504559, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x79, 0x97, 0x7b, 0x30, 0x8a, 0x94, 0x93, 0xa7, 0x6e, 0xfc, 0x9e, 0x39, 0xc3, 0xd5, 0x90, 0x25, 0xb8, 0xb9, 0xf2, 0x85, 0xb4, 0x1f, 0xcd, 0x71, 0xf, 0xfa, 0x7b, 0x74, 0x8, 0x5c, 0x53, 0x7f}}
return a, nil
}
@ -4584,7 +4584,7 @@ func confLocaleLocale_enUsIni() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "conf/locale/locale_en-US.ini", size: 69630, mode: os.FileMode(0644), modTime: time.Unix(1599653855, 0)}
info := bindataFileInfo{name: "conf/locale/locale_en-US.ini", size: 69630, mode: os.FileMode(0644), modTime: time.Unix(1600417296, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xea, 0x6c, 0x8a, 0xc4, 0x1, 0x79, 0xc1, 0x39, 0xaf, 0x1, 0xfc, 0x47, 0x8e, 0x3c, 0xce, 0x35, 0x7f, 0xfb, 0x24, 0xd3, 0x26, 0xf0, 0xe3, 0x1a, 0x6d, 0xdf, 0x91, 0x92, 0xd8, 0x2a, 0x93, 0x95}}
return a, nil
}
@ -4604,7 +4604,7 @@ func confLocaleLocale_esEsIni() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "conf/locale/locale_es-ES.ini", size: 74572, mode: os.FileMode(0644), modTime: time.Unix(1600268328, 0)}
info := bindataFileInfo{name: "conf/locale/locale_es-ES.ini", size: 74572, mode: os.FileMode(0644), modTime: time.Unix(1600500781, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x10, 0x8e, 0x14, 0x2e, 0xa, 0xdc, 0xf0, 0x4d, 0x11, 0x9e, 0x48, 0xe7, 0x7b, 0x3f, 0x84, 0x2d, 0xab, 0x25, 0x2a, 0x11, 0x4d, 0x20, 0x36, 0x49, 0xa8, 0xe3, 0x75, 0x85, 0x54, 0xac, 0x95, 0xa8}}
return a, nil
}
@ -4884,7 +4884,7 @@ func confLocaleLocale_ptPtIni() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "conf/locale/locale_pt-PT.ini", size: 73358, mode: os.FileMode(0644), modTime: time.Unix(1600268328, 0)}
info := bindataFileInfo{name: "conf/locale/locale_pt-PT.ini", size: 73358, mode: os.FileMode(0644), modTime: time.Unix(1600500781, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x9e, 0x9, 0x9c, 0xec, 0x51, 0x68, 0x0, 0xab, 0xc1, 0xa2, 0xcf, 0x1b, 0x9, 0xca, 0x7b, 0xbb, 0x3a, 0x38, 0x50, 0x3e, 0x29, 0xc4, 0xbe, 0x84, 0xc, 0x43, 0x35, 0xe8, 0x34, 0x20, 0x31, 0x72}}
return a, nil
}
@ -5004,7 +5004,7 @@ func confLocaleLocale_ukUaIni() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "conf/locale/locale_uk-UA.ini", size: 100902, mode: os.FileMode(0644), modTime: time.Unix(1600268328, 0)}
info := bindataFileInfo{name: "conf/locale/locale_uk-UA.ini", size: 100902, mode: os.FileMode(0644), modTime: time.Unix(1600417296, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xb9, 0x96, 0x2f, 0x7f, 0x77, 0x58, 0xac, 0x25, 0x18, 0xbb, 0xe2, 0x9f, 0x19, 0x24, 0x75, 0x81, 0xf0, 0xb2, 0x61, 0x12, 0xad, 0xf6, 0xa9, 0xe7, 0xdc, 0x78, 0x4b, 0x98, 0x5c, 0x58, 0x24, 0xc1}}
return a, nil
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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/conf"
"gogs.io/gogs/internal/db"
"gogs.io/gogs/internal/tool"
"gogs.io/gogs/internal/errutil"
)
func IsAPIPath(url string) bool {
return strings.HasPrefix(url, "/api/")
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
)
// 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
}
var _ errutil.NotFound = (*ErrBadCredentials)(nil)
// 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
}
}
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
}
}
// 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])
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
}
return u, true, false
}
}
return nil, false, false
}
u, err := db.GetUserByID(uid)
if err != nil {
log.Error("GetUserByID: %v", err)
return nil, false, false
}
return u, false, isTokenAuth
func IsErrBadCredentials(err error) bool {
_, ok := err.(ErrBadCredentials)
return ok
}
func (err ErrBadCredentials) Error() string {
return fmt.Sprintf("bad credentials: %v", err.Args)
}
func (ErrBadCredentials) NotFound() bool {
return true
}
// 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
}
// 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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 (ls *Source) sanitizedUserQuery(username string) (string, bool) {
func (c *Config) SecurityProtocolName() string {
return SecurityProtocolName(c.SecurityProtocol)
}
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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -7,12 +7,18 @@ package context
import (
"net/http"
"net/url"
"strings"
"github.com/go-macaron/csrf"
"github.com/go-macaron/session"
gouuid "github.com/satori/go.uuid"
"gopkg.in/macaron.v1"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/auth"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/db"
"gogs.io/gogs/internal/tool"
)
type ToggleOptions struct {
@ -49,7 +55,7 @@ func Toggle(options *ToggleOptions) macaron.Handler {
return
}
if !options.SignOutRequired && !options.DisableCSRF && c.Req.Method == "POST" && !auth.IsAPIPath(c.Req.URL.Path) {
if !options.SignOutRequired && !options.DisableCSRF && c.Req.Method == "POST" && !isAPIPath(c.Req.URL.Path) {
csrf.Validate(c.Context, c.csrf)
if c.Written() {
return
@ -59,7 +65,7 @@ func Toggle(options *ToggleOptions) macaron.Handler {
if options.SignInRequired {
if !c.IsLogged {
// Restrict API calls with error message.
if auth.IsAPIPath(c.Req.URL.Path) {
if isAPIPath(c.Req.URL.Path) {
c.JSON(http.StatusForbidden, map[string]string{
"message": "Only authenticated user is allowed to call APIs.",
})
@ -77,7 +83,7 @@ func Toggle(options *ToggleOptions) macaron.Handler {
}
// Redirect to log in page if auto-signin info is provided and has not signed in.
if !options.SignOutRequired && !c.IsLogged && !auth.IsAPIPath(c.Req.URL.Path) &&
if !options.SignOutRequired && !c.IsLogged && !isAPIPath(c.Req.URL.Path) &&
len(c.GetCookie(conf.Security.CookieUsername)) > 0 {
c.SetCookie("redirect_to", url.QueryEscape(conf.Server.Subpath+c.Req.RequestURI), 0, conf.Server.Subpath)
c.RedirectSubpath("/user/login")
@ -93,3 +99,133 @@ func Toggle(options *ToggleOptions) macaron.Handler {
}
}
}
func isAPIPath(url string) bool {
return strings.HasPrefix(url, "/api/")
}
// authenticatedUserID returns the ID of the authenticated user, along with a bool value
// which indicates whether the user uses token authentication.
func authenticatedUserID(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
}
}
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
}
// authenticatedUser returns the user object of the authenticated user, along with two bool values
// which indicate whether the user uses HTTP Basic Authentication or token authentication respectively.
func authenticatedUser(ctx *macaron.Context, sess session.Store) (_ *db.User, isBasicAuth bool, isTokenAuth bool) {
if !db.HasEngine {
return nil, false, false
}
uid, isTokenAuth := authenticatedUserID(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
}
}
// 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])
u, err := db.Users.Authenticate(uname, passwd, -1)
if err != nil {
if !auth.IsErrBadCredentials(err) {
log.Error("Failed to authenticate user: %v", err)
}
return nil, false, false
}
return u, true, false
}
}
return nil, false, false
}
u, err := db.GetUserByID(uid)
if err != nil {
log.Error("GetUserByID: %v", err)
return nil, false, false
}
return u, false, isTokenAuth
}

View File

@ -18,7 +18,6 @@ import (
"gopkg.in/macaron.v1"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/auth"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/db"
"gogs.io/gogs/internal/errutil"
@ -255,7 +254,7 @@ func Contexter() macaron.Handler {
}
// Get user from session or header when possible
c.User, c.IsBasicAuth, c.IsTokenAuth = auth.SignedInUser(c.Context, c.Session)
c.User, c.IsBasicAuth, c.IsTokenAuth = authenticatedUser(c.Context, c.Session)
if c.User != nil {
c.IsLogged = true

View File

@ -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,
},
}

View File

@ -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)
}

View File

@ -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}
}

View File

@ -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
})

View File

@ -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
return s.Type == auth.GitHub
}
func (s *LoginSource) HasTLS() bool {
return ((s.IsLDAP() || s.IsDLDAP()) &&
s.LDAP().SecurityProtocol > ldap.SecurityProtocolUnencrypted) ||
s.IsSMTP()
func (s *LoginSource) LDAP() *ldap.Config {
return s.Provider.Config().(*ldap.Config)
}
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) SMTP() *smtp.Config {
return s.Provider.Config().(*smtp.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) PAM() *pam.Config {
return s.Provider.Config().(*pam.Config)
}
func (s *LoginSource) LDAP() *LDAPConfig {
return s.Config.(*LDAPConfig)
}
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")

View File

@ -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 },

View File

@ -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) {

View File

@ -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}

View File

@ -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
} else {
// Non-local login source is always greater than 0.
if loginSourceID <= 0 {
return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}}
}
_, err = authenticateViaLoginSource(source, login, password, false)
if err != nil {
return nil, errors.Wrap(err, "authenticate via login source")
}
return user, nil
authSourceID = loginSourceID
createNewUser = true
}
// Non-local login source is always greater than 0.
if loginSourceID <= 0 {
return nil, ErrUserNotExist{args: map[string]interface{}{"login": login}}
}
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 err != nil {
return nil, errors.Wrap(err, "authenticate via login source")
if !source.IsActived {
return nil, errors.Errorf("login source %d is not activated", source.ID)
}
return user, nil
extAccount, err := source.Provider.Authenticate(login, password)
if err != nil {
return nil, err
}
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()

View File

@ -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)
}

View File

@ -12,7 +12,11 @@ import (
"github.com/unknwon/com"
log "unknwon.dev/clog/v2"
"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/conf"
"gogs.io/gogs/internal/context"
"gogs.io/gogs/internal/db"
@ -48,16 +52,16 @@ type dropdownItem struct {
var (
authSources = []dropdownItem{
{db.LoginNames[db.LoginLDAP], db.LoginLDAP},
{db.LoginNames[db.LoginDLDAP], db.LoginDLDAP},
{db.LoginNames[db.LoginSMTP], db.LoginSMTP},
{db.LoginNames[db.LoginPAM], db.LoginPAM},
{db.LoginNames[db.LoginGitHub], db.LoginGitHub},
{auth.Name(auth.LDAP), auth.LDAP},
{auth.Name(auth.DLDAP), auth.DLDAP},
{auth.Name(auth.SMTP), auth.SMTP},
{auth.Name(auth.PAM), auth.PAM},
{auth.Name(auth.GitHub), auth.GitHub},
}
securityProtocols = []dropdownItem{
{db.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
{db.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
{db.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
{ldap.SecurityProtocolName(ldap.SecurityProtocolUnencrypted), ldap.SecurityProtocolUnencrypted},
{ldap.SecurityProtocolName(ldap.SecurityProtocolLDAPS), ldap.SecurityProtocolLDAPS},
{ldap.SecurityProtocolName(ldap.SecurityProtocolStartTLS), ldap.SecurityProtocolStartTLS},
}
)
@ -66,47 +70,45 @@ func NewAuthSource(c *context.Context) {
c.PageIs("Admin")
c.PageIs("AdminAuthentications")
c.Data["type"] = db.LoginLDAP
c.Data["CurrentTypeName"] = db.LoginNames[db.LoginLDAP]
c.Data["CurrentSecurityProtocol"] = db.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
c.Data["type"] = auth.LDAP
c.Data["CurrentTypeName"] = auth.Name(auth.LDAP)
c.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolName(ldap.SecurityProtocolUnencrypted)
c.Data["smtp_auth"] = "PLAIN"
c.Data["is_active"] = true
c.Data["is_default"] = true
c.Data["AuthSources"] = authSources
c.Data["SecurityProtocols"] = securityProtocols
c.Data["SMTPAuths"] = db.SMTPAuths
c.Data["SMTPAuths"] = smtp.AuthTypes
c.Success(AUTH_NEW)
}
func parseLDAPConfig(f form.Authentication) *db.LDAPConfig {
return &db.LDAPConfig{
Source: ldap.Source{
Host: f.Host,
Port: f.Port,
SecurityProtocol: ldap.SecurityProtocol(f.SecurityProtocol),
SkipVerify: f.SkipVerify,
BindDN: f.BindDN,
UserDN: f.UserDN,
BindPassword: f.BindPassword,
UserBase: f.UserBase,
AttributeUsername: f.AttributeUsername,
AttributeName: f.AttributeName,
AttributeSurname: f.AttributeSurname,
AttributeMail: f.AttributeMail,
AttributesInBind: f.AttributesInBind,
Filter: f.Filter,
GroupEnabled: f.GroupEnabled,
GroupDN: f.GroupDN,
GroupFilter: f.GroupFilter,
GroupMemberUID: f.GroupMemberUID,
UserUID: f.UserUID,
AdminFilter: f.AdminFilter,
},
func parseLDAPConfig(f form.Authentication) *ldap.Config {
return &ldap.Config{
Host: f.Host,
Port: f.Port,
SecurityProtocol: ldap.SecurityProtocol(f.SecurityProtocol),
SkipVerify: f.SkipVerify,
BindDN: f.BindDN,
UserDN: f.UserDN,
BindPassword: f.BindPassword,
UserBase: f.UserBase,
AttributeUsername: f.AttributeUsername,
AttributeName: f.AttributeName,
AttributeSurname: f.AttributeSurname,
AttributeMail: f.AttributeMail,
AttributesInBind: f.AttributesInBind,
Filter: f.Filter,
GroupEnabled: f.GroupEnabled,
GroupDN: f.GroupDN,
GroupFilter: f.GroupFilter,
GroupMemberUID: f.GroupMemberUID,
UserUID: f.UserUID,
AdminFilter: f.AdminFilter,
}
}
func parseSMTPConfig(f form.Authentication) *db.SMTPConfig {
return &db.SMTPConfig{
func parseSMTPConfig(f form.Authentication) *smtp.Config {
return &smtp.Config{
Auth: f.SMTPAuth,
Host: f.SMTPHost,
Port: f.SMTPPort,
@ -121,29 +123,31 @@ func NewAuthSourcePost(c *context.Context, f form.Authentication) {
c.PageIs("Admin")
c.PageIs("AdminAuthentications")
c.Data["CurrentTypeName"] = db.LoginNames[db.LoginType(f.Type)]
c.Data["CurrentSecurityProtocol"] = db.SecurityProtocolNames[ldap.SecurityProtocol(f.SecurityProtocol)]
c.Data["CurrentTypeName"] = auth.Name(auth.Type(f.Type))
c.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolName(ldap.SecurityProtocol(f.SecurityProtocol))
c.Data["AuthSources"] = authSources
c.Data["SecurityProtocols"] = securityProtocols
c.Data["SMTPAuths"] = db.SMTPAuths
c.Data["SMTPAuths"] = smtp.AuthTypes
hasTLS := false
var config interface{}
switch db.LoginType(f.Type) {
case db.LoginLDAP, db.LoginDLDAP:
switch auth.Type(f.Type) {
case auth.LDAP, auth.DLDAP:
config = parseLDAPConfig(f)
hasTLS = ldap.SecurityProtocol(f.SecurityProtocol) > ldap.SecurityProtocolUnencrypted
case db.LoginSMTP:
case auth.SMTP:
config = parseSMTPConfig(f)
hasTLS = true
case db.LoginPAM:
config = &db.PAMConfig{
case auth.PAM:
config = &pam.Config{
ServiceName: f.PAMServiceName,
}
case db.LoginGitHub:
config = &db.GitHubConfig{
case auth.GitHub:
config = &github.Config{
APIEndpoint: strings.TrimSuffix(f.GitHubAPIEndpoint, "/") + "/",
SkipVerify: f.SkipVerify,
}
hasTLS = true
default:
c.Status(http.StatusBadRequest)
return
@ -156,7 +160,7 @@ func NewAuthSourcePost(c *context.Context, f form.Authentication) {
}
source, err := db.LoginSources.Create(db.CreateLoginSourceOpts{
Type: db.LoginType(f.Type),
Type: auth.Type(f.Type),
Name: f.Name,
Activated: f.IsActive,
Default: f.IsDefault,
@ -192,7 +196,7 @@ func EditAuthSource(c *context.Context) {
c.PageIs("AdminAuthentications")
c.Data["SecurityProtocols"] = securityProtocols
c.Data["SMTPAuths"] = db.SMTPAuths
c.Data["SMTPAuths"] = smtp.AuthTypes
source, err := db.LoginSources.GetByID(c.ParamsInt64(":authid"))
if err != nil {
@ -200,7 +204,7 @@ func EditAuthSource(c *context.Context) {
return
}
c.Data["Source"] = source
c.Data["HasTLS"] = source.HasTLS()
c.Data["HasTLS"] = source.Provider.HasTLS()
c.Success(AUTH_EDIT)
}
@ -210,7 +214,7 @@ func EditAuthSourcePost(c *context.Context, f form.Authentication) {
c.PageIs("Admin")
c.PageIs("AdminAuthentications")
c.Data["SMTPAuths"] = db.SMTPAuths
c.Data["SMTPAuths"] = smtp.AuthTypes
source, err := db.LoginSources.GetByID(c.ParamsInt64(":authid"))
if err != nil {
@ -218,27 +222,30 @@ func EditAuthSourcePost(c *context.Context, f form.Authentication) {
return
}
c.Data["Source"] = source
c.Data["HasTLS"] = source.HasTLS()
c.Data["HasTLS"] = source.Provider.HasTLS()
if c.HasError() {
c.Success(AUTH_EDIT)
return
}
var config interface{}
switch db.LoginType(f.Type) {
case db.LoginLDAP, db.LoginDLDAP:
config = parseLDAPConfig(f)
case db.LoginSMTP:
config = parseSMTPConfig(f)
case db.LoginPAM:
config = &db.PAMConfig{
var provider auth.Provider
switch auth.Type(f.Type) {
case auth.LDAP:
provider = ldap.NewProvider(false, parseLDAPConfig(f))
case auth.DLDAP:
provider = ldap.NewProvider(true, parseLDAPConfig(f))
case auth.SMTP:
provider = smtp.NewProvider(parseSMTPConfig(f))
case auth.PAM:
provider = pam.NewProvider(&pam.Config{
ServiceName: f.PAMServiceName,
}
case db.LoginGitHub:
config = &db.GitHubConfig{
})
case auth.GitHub:
provider = github.NewProvider(&github.Config{
APIEndpoint: strings.TrimSuffix(f.GitHubAPIEndpoint, "/") + "/",
}
SkipVerify: f.SkipVerify,
})
default:
c.Status(http.StatusBadRequest)
return
@ -247,7 +254,7 @@ func EditAuthSourcePost(c *context.Context, f form.Authentication) {
source.Name = f.Name
source.IsActived = f.IsActive
source.IsDefault = f.IsDefault
source.Config = config
source.Provider = provider
if err := db.LoginSources.Save(source); err != nil {
c.Error(err, "update login source")
return

View File

@ -11,6 +11,7 @@ import (
"gopkg.in/macaron.v1"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/auth"
"gogs.io/gogs/internal/authutil"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/db"
@ -59,7 +60,7 @@ func authenticate() macaron.Handler {
}
user, err := db.Users.Authenticate(username, password, -1)
if err != nil && !db.IsErrUserNotExist(err) {
if err != nil && !auth.IsErrBadCredentials(err) {
internalServerError(c.Resp)
log.Error("Failed to authenticate user [name: %s]: %v", username, err)
return
@ -71,7 +72,7 @@ func authenticate() macaron.Handler {
}
// If username and password authentication failed, try again using username as an access token.
if db.IsErrUserNotExist(err) {
if auth.IsErrBadCredentials(err) {
token, err := db.AccessTokens.GetBySHA(username)
if err != nil {
if db.IsErrAccessTokenNotExist(err) {

View File

@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/assert"
"gopkg.in/macaron.v1"
"gogs.io/gogs/internal/auth"
"gogs.io/gogs/internal/db"
"gogs.io/gogs/internal/lfsutil"
)
@ -70,7 +71,7 @@ func Test_authenticate(t *testing.T) {
},
mockUsersStore: &db.MockUsersStore{
MockAuthenticate: func(username, password string, loginSourceID int64) (*db.User, error) {
return nil, db.ErrUserNotExist{}
return nil, auth.ErrBadCredentials{}
},
},
mockAccessTokensStore: &db.MockAccessTokensStore{
@ -112,7 +113,7 @@ func Test_authenticate(t *testing.T) {
},
mockUsersStore: &db.MockUsersStore{
MockAuthenticate: func(username, password string, loginSourceID int64) (*db.User, error) {
return nil, db.ErrUserNotExist{}
return nil, auth.ErrBadCredentials{}
},
MockGetByID: func(id int64) (*db.User, error) {
return &db.User{ID: 1, Name: "unknwon"}, nil

View File

@ -9,6 +9,7 @@ import (
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/auth"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/context"
"gogs.io/gogs/internal/db"
@ -109,7 +110,7 @@ func SettingsDelete(c *context.Context) {
org := c.Org.Organization
if c.Req.Method == "POST" {
if _, err := db.Users.Authenticate(c.User.Name, c.Query("password"), c.User.LoginSource); err != nil {
if db.IsErrUserNotExist(err) {
if auth.IsErrBadCredentials(err) {
c.RenderWithErr(c.Tr("form.enterred_invalid_password"), SETTINGS_DELETE, nil)
} else {
c.Error(err, "authenticate user")

View File

@ -20,6 +20,7 @@ import (
"gopkg.in/macaron.v1"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/auth"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/db"
"gogs.io/gogs/internal/lazyregexp"
@ -122,7 +123,7 @@ func HTTPContexter() macaron.Handler {
}
authUser, err := db.Users.Authenticate(authUsername, authPassword, -1)
if err != nil && !db.IsErrUserNotExist(err) {
if err != nil && !auth.IsErrBadCredentials(err) {
c.Status(http.StatusInternalServerError)
log.Error("Failed to authenticate user [name: %s]: %v", authUsername, err)
return

View File

@ -12,6 +12,7 @@ import (
"github.com/pkg/errors"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/auth"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/context"
"gogs.io/gogs/internal/db"
@ -163,7 +164,7 @@ func LoginPost(c *context.Context, f form.SignIn) {
u, err := db.Users.Authenticate(f.UserName, f.Password, f.LoginSource)
if err != nil {
switch errors.Cause(err).(type) {
case db.ErrUserNotExist:
case auth.ErrBadCredentials:
c.FormErr("UserName", "Password")
c.RenderWithErr(c.Tr("form.username_password_incorrect"), LOGIN, &f)
case db.ErrLoginSourceMismatch:

View File

@ -18,6 +18,7 @@ import (
"github.com/unknwon/com"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/auth"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/context"
"gogs.io/gogs/internal/cryptoutil"
@ -640,7 +641,7 @@ func SettingsDelete(c *context.Context) {
if c.Req.Method == "POST" {
if _, err := db.Users.Authenticate(c.User.Name, c.Query("password"), c.User.LoginSource); err != nil {
if db.IsErrUserNotExist(err) {
if auth.IsErrBadCredentials(err) {
c.RenderWithErr(c.Tr("form.enterred_invalid_password"), SETTINGS_DELETE, nil)
} else {
c.Errorf(err, "authenticate user")

View File

@ -1089,6 +1089,7 @@ function initAdmin() {
break;
case "6": //GITHUB
$(".github").show();
$(".has-tls").show();
break;
}

View File

@ -180,13 +180,13 @@
<div class="inline field {{if not .Source.IsSMTP}}hide{{end}}">
<div class="ui checkbox">
<label><strong>{{.i18n.Tr "admin.auths.enable_tls"}}</strong></label>
<input name="tls" type="checkbox" {{if .Source.UseTLS}}checked{{end}}>
<input name="tls" type="checkbox" {{if .Source.Provider.UseTLS}}checked{{end}}>
</div>
</div>
<div class="has-tls inline field {{if not .HasTLS}}hide{{end}}">
<div class="ui checkbox">
<label><strong>{{.i18n.Tr "admin.auths.skip_tls_verify"}}</strong></label>
<input name="skip_verify" type="checkbox" {{if .Source.SkipVerify}}checked{{end}}>
<input name="skip_verify" type="checkbox" {{if .Source.Provider.SkipTLSVerify}}checked{{end}}>
</div>
</div>
<div class="inline field">