diff options
author | Unknwon <u@gogs.io> | 2017-04-04 19:29:59 -0400 |
---|---|---|
committer | Unknwon <u@gogs.io> | 2017-04-04 19:29:59 -0400 |
commit | d05395fe906dad7741201faa69a54fef538deda9 (patch) | |
tree | 11dae6c5c9b40b8ce85c7294bd0309c03cb1199e /pkg/auth | |
parent | 37b10666dea98cebf75d0c6f11ee87211ef94703 (diff) |
Refactoring: rename modules -> pkg
Reasons to change:
1. Shorter than 'modules'
2. More generally used by other Go projects
3. Corresponds to the naming of '$GOPATH/pkg' directory
Diffstat (limited to 'pkg/auth')
-rw-r--r-- | pkg/auth/auth.go | 147 | ||||
-rw-r--r-- | pkg/auth/ldap/README.md | 101 | ||||
-rw-r--r-- | pkg/auth/ldap/ldap.go | 249 | ||||
-rw-r--r-- | pkg/auth/pam/pam.go | 35 | ||||
-rw-r--r-- | pkg/auth/pam/pam_stub.go | 15 |
5 files changed, 547 insertions, 0 deletions
diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go new file mode 100644 index 00000000..fd4d71c9 --- /dev/null +++ b/pkg/auth/auth.go @@ -0,0 +1,147 @@ +// 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 auth + +import ( + "strings" + "time" + + "github.com/go-macaron/session" + gouuid "github.com/satori/go.uuid" + log "gopkg.in/clog.v1" + "gopkg.in/macaron.v1" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/base" + "github.com/gogits/gogs/pkg/setting" +) + +func IsAPIPath(url string) bool { + return strings.HasPrefix(url, "/api/") +} + +// SignedInID returns the id of signed in user. +func SignedInID(ctx *macaron.Context, sess session.Store) int64 { + if !models.HasEngine { + return 0 + } + + // Check access token. + if IsAPIPath(ctx.Req.URL.Path) { + tokenSHA := ctx.Query("token") + if len(tokenSHA) == 0 { + // Well, check with header again. + auHead := ctx.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 := models.GetAccessTokenBySHA(tokenSHA) + if err != nil { + if !models.IsErrAccessTokenNotExist(err) && !models.IsErrAccessTokenEmpty(err) { + log.Error(2, "GetAccessTokenBySHA: %v", err) + } + return 0 + } + t.Updated = time.Now() + if err = models.UpdateAccessToken(t); err != nil { + log.Error(2, "UpdateAccessToken: %v", err) + } + return t.UID + } + } + + uid := sess.Get("uid") + if uid == nil { + return 0 + } + if id, ok := uid.(int64); ok { + if _, err := models.GetUserByID(id); err != nil { + if !errors.IsUserNotExist(err) { + log.Error(2, "GetUserByID: %v", err) + } + return 0 + } + return id + } + return 0 +} + +// SignedInUser returns the user object of signed user. +// It returns a bool value to indicate whether user uses basic auth or not. +func SignedInUser(ctx *macaron.Context, sess session.Store) (*models.User, bool) { + if !models.HasEngine { + return nil, false + } + + uid := SignedInID(ctx, sess) + + if uid <= 0 { + if setting.Service.EnableReverseProxyAuth { + webAuthUser := ctx.Req.Header.Get(setting.ReverseProxyAuthUser) + if len(webAuthUser) > 0 { + u, err := models.GetUserByName(webAuthUser) + if err != nil { + if !errors.IsUserNotExist(err) { + log.Error(4, "GetUserByName: %v", err) + return nil, false + } + + // Check if enabled auto-registration. + if setting.Service.EnableReverseProxyAutoRegister { + u := &models.User{ + Name: webAuthUser, + Email: gouuid.NewV4().String() + "@localhost", + Passwd: webAuthUser, + IsActive: true, + } + if err = models.CreateUser(u); err != nil { + // FIXME: should I create a system notice? + log.Error(4, "CreateUser: %v", err) + return nil, false + } else { + return u, false + } + } + } + return u, 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, _ := base.BasicAuthDecode(auths[1]) + + u, err := models.UserSignIn(uname, passwd) + if err != nil { + if !errors.IsUserNotExist(err) { + log.Error(4, "UserSignIn: %v", err) + } + return nil, false + } + + return u, true + } + } + return nil, false + } + + u, err := models.GetUserByID(uid) + if err != nil { + log.Error(4, "GetUserById: %v", err) + return nil, false + } + return u, false +} diff --git a/pkg/auth/ldap/README.md b/pkg/auth/ldap/README.md new file mode 100644 index 00000000..3a3e0204 --- /dev/null +++ b/pkg/auth/ldap/README.md @@ -0,0 +1,101 @@ +Gogs LDAP Authentication Module +=============================== + +## About + +This authentication module attempts to authorize and authenticate a user +against an LDAP server. It provides two methods of authentication: LDAP via +BindDN, and LDAP simple authentication. + +LDAP via BindDN functions like most LDAP authentication systems. First, it +queries the LDAP server using a Bind DN and searches for the user that is +attempting to sign in. If the user is found, the module attempts to bind to the +server using the user's supplied credentials. If this succeeds, the user has +been authenticated, and his account information is retrieved and passed to the +Gogs login infrastructure. + +LDAP simple authentication does not utilize a Bind DN. Instead, it binds +directly with the LDAP server using the user's supplied credentials. If the bind +succeeds and no filter rules out the user, the user is authenticated. + +LDAP via BindDN is recommended for most users. By using a Bind DN, the server +can perform authorization by restricting which entries the Bind DN account can +read. Further, using a Bind DN with reduced permissions can reduce security risk +in the face of application bugs. + +## Usage + +To use this module, add an LDAP authentication source via the Authentications +section in the admin panel. Both the LDAP via BindDN and the simple auth LDAP +share the following fields: + +* Authorization Name **(required)** + * A name to assign to the new method of authorization. + +* Host **(required)** + * The address where the LDAP server can be reached. + * Example: mydomain.com + +* Port **(required)** + * The port to use when connecting to the server. + * Example: 636 + +* Enable TLS Encryption (optional) + * Whether to use TLS when connecting to the LDAP server. + +* Admin Filter (optional) + * An LDAP filter specifying if a user should be given administrator + privileges. If a user accounts passes the filter, the user will be + privileged as an administrator. + * Example: (objectClass=adminAccount) + +* First name attribute (optional) + * The attribute of the user's LDAP record containing the user's first name. + This will be used to populate their account information. + * Example: givenName + +* Surname attribute (optional) + * The attribute of the user's LDAP record containing the user's surname This + will be used to populate their account information. + * Example: sn + +* E-mail attribute **(required)** + * The attribute of the user's LDAP record containing the user's email + address. This will be used to populate their account information. + * Example: mail + +**LDAP via BindDN** adds the following fields: + +* Bind DN (optional) + * The DN to bind to the LDAP server with when searching for the user. This + may be left blank to perform an anonymous search. + * Example: cn=Search,dc=mydomain,dc=com + +* Bind Password (optional) + * The password for the Bind DN specified above, if any. _Note: The password + is stored in plaintext at the server. As such, ensure that your Bind DN + has as few privileges as possible._ + +* User Search Base **(required)** + * The LDAP base at which user accounts will be searched for. + * Example: ou=Users,dc=mydomain,dc=com + +* User Filter **(required)** + * An LDAP filter declaring how to find the user record that is attempting to + authenticate. The '%s' matching parameter will be substituted with the + user's username. + * Example: (&(objectClass=posixAccount)(uid=%s)) + +**LDAP using simple auth** adds the following fields: + +* User DN **(required)** + * A template to use as the user's DN. The `%s` matching parameter will be + substituted with the user's username. + * Example: cn=%s,ou=Users,dc=mydomain,dc=com + * Example: uid=%s,ou=Users,dc=mydomain,dc=com + +* User Filter **(required)** + * An LDAP filter declaring when a user should be allowed to log in. The `%s` + matching parameter will be substituted with the user's username. + * Example: (&(objectClass=posixAccount)(cn=%s)) + * Example: (&(objectClass=posixAccount)(uid=%s)) diff --git a/pkg/auth/ldap/ldap.go b/pkg/auth/ldap/ldap.go new file mode 100644 index 00000000..78cd66a4 --- /dev/null +++ b/pkg/auth/ldap/ldap.go @@ -0,0 +1,249 @@ +// 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 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 ( + "crypto/tls" + "fmt" + "strings" + + log "gopkg.in/clog.v1" + "gopkg.in/ldap.v2" +) + +type SecurityProtocol int + +// Note: new type must be added at the end of list to maintain compatibility. +const ( + SECURITY_PROTOCOL_UNENCRYPTED SecurityProtocol = iota + SECURITY_PROTOCOL_LDAPS + SECURITY_PROTOCOL_START_TLS +) + +// Basic LDAP authentication service +type Source struct { + Name string // canonical name (ie. corporate.ad) + Host string // LDAP host + Port int // port number + SecurityProtocol SecurityProtocol + SkipVerify bool + BindDN string // DN to bind with + BindPassword string // Bind DN password + UserBase string // Base search path for users + UserDN string // Template for the DN of the user for simple auth + 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) + Filter string // Query filter to validate entry + AdminFilter string // Query filter to check if user is admin + Enabled bool // if this source is disabled +} + +func (ls *Source) sanitizedUserQuery(username string) (string, bool) { + // See http://tools.ietf.org/search/rfc4515 + badCharacters := "\x00()*\\" + if strings.ContainsAny(username, badCharacters) { + log.Trace("Username contains invalid query characters: %s", username) + return "", false + } + + return fmt.Sprintf(ls.Filter, username), true +} + +func (ls *Source) 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, " ") { + log.Trace("Username contains invalid query characters: %s", username) + return "", false + } + + return fmt.Sprintf(ls.UserDN, username), true +} + +func (ls *Source) findUserDN(l *ldap.Conn, name string) (string, bool) { + log.Trace("Search for LDAP user: %s", name) + if ls.BindDN != "" && ls.BindPassword != "" { + err := l.Bind(ls.BindDN, ls.BindPassword) + if err != nil { + log.Trace("Failed to bind as BindDN '%s': %v", ls.BindDN, err) + return "", false + } + log.Trace("Bound as BindDN: %s", ls.BindDN) + } else { + log.Trace("Proceeding with anonymous LDAP search") + } + + // A search for the user. + userFilter, ok := ls.sanitizedUserQuery(name) + if !ok { + return "", false + } + + log.Trace("Searching for DN using filter '%s' and base '%s'", userFilter, ls.UserBase) + search := ldap.NewSearchRequest( + ls.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("Failed search using filter '%s': %v", userFilter, err) + return "", false + } else if len(sr.Entries) > 1 { + log.Trace("Filter '%s' returned more than one user", userFilter) + return "", false + } + + userDN := sr.Entries[0].DN + if userDN == "" { + log.Error(4, "LDAP search was successful, but found no DN!") + return "", false + } + + return userDN, true +} + +func dial(ls *Source) (*ldap.Conn, error) { + log.Trace("Dialing LDAP with security protocol '%v' without verifying: %v", ls.SecurityProtocol, ls.SkipVerify) + + tlsCfg := &tls.Config{ + ServerName: ls.Host, + InsecureSkipVerify: ls.SkipVerify, + } + if ls.SecurityProtocol == SECURITY_PROTOCOL_LDAPS { + return ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ls.Host, ls.Port), tlsCfg) + } + + conn, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ls.Host, ls.Port)) + if err != nil { + return nil, fmt.Errorf("Dial: %v", err) + } + + if ls.SecurityProtocol == SECURITY_PROTOCOL_START_TLS { + if err = conn.StartTLS(tlsCfg); err != nil { + conn.Close() + return nil, fmt.Errorf("StartTLS: %v", err) + } + } + + return conn, nil +} + +func bindUser(l *ldap.Conn, userDN, passwd string) error { + log.Trace("Binding with userDN: %s", userDN) + err := l.Bind(userDN, passwd) + if err != nil { + log.Trace("LDAP authentication failed for '%s': %v", userDN, err) + return err + } + log.Trace("Bound successfully with userDN: %s", userDN) + 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) { + // See https://tools.ietf.org/search/rfc4513#section-5.1.2 + if len(passwd) == 0 { + log.Trace("authentication failed for '%s' with empty password") + return "", "", "", "", false, false + } + l, err := dial(ls) + if err != nil { + log.Error(4, "LDAP connect failed for '%s': %v", ls.Host, err) + ls.Enabled = false + return "", "", "", "", false, false + } + defer l.Close() + + var userDN string + if directBind { + log.Trace("LDAP will bind directly via UserDN template: %s", ls.UserDN) + + var ok bool + userDN, ok = ls.sanitizedUserDN(name) + if !ok { + return "", "", "", "", false, false + } + } else { + log.Trace("LDAP will use BindDN") + + var found bool + userDN, found = ls.findUserDN(l, name) + if !found { + return "", "", "", "", false, false + } + } + + if directBind || !ls.AttributesInBind { + // binds user (checking password) before looking-up attributes in user context + err = bindUser(l, userDN, passwd) + if err != nil { + return "", "", "", "", false, false + } + } + + userFilter, ok := ls.sanitizedUserQuery(name) + if !ok { + return "", "", "", "", false, false + } + + log.Trace("Fetching attributes '%v', '%v', '%v', '%v' with filter '%s' and base '%s'", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, userFilter, userDN) + search := ldap.NewSearchRequest( + userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter, + []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail}, + nil) + + sr, err := l.Search(search) + if err != nil { + log.Error(4, "LDAP search failed: %v", err) + return "", "", "", "", false, false + } else if len(sr.Entries) < 1 { + if directBind { + log.Error(4, "User filter inhibited user login") + } else { + log.Error(4, "LDAP search failed: 0 entries") + } + + 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) + + isAdmin := false + if len(ls.AdminFilter) > 0 { + log.Trace("Checking admin with filter '%s' and base '%s'", ls.AdminFilter, userDN) + search = ldap.NewSearchRequest( + userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ls.AdminFilter, + []string{ls.AttributeName}, + nil) + + sr, err = l.Search(search) + if err != nil { + log.Error(4, "LDAP admin search failed: %v", err) + } else if len(sr.Entries) < 1 { + log.Error(4, "LDAP admin search failed: 0 entries") + } else { + isAdmin = true + } + } + + if !directBind && ls.AttributesInBind { + // binds user (checking password) after looking-up attributes in BindDN context + err = bindUser(l, userDN, passwd) + if err != nil { + return "", "", "", "", false, false + } + } + + return username, firstname, surname, mail, isAdmin, true +} diff --git a/pkg/auth/pam/pam.go b/pkg/auth/pam/pam.go new file mode 100644 index 00000000..7f326d42 --- /dev/null +++ b/pkg/auth/pam/pam.go @@ -0,0 +1,35 @@ +// +build pam + +// 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 + +import ( + "errors" + + "github.com/msteinert/pam" +) + +func PAMAuth(serviceName, userName, passwd string) error { + t, err := pam.StartFunc(serviceName, userName, func(s pam.Style, msg string) (string, error) { + switch s { + case pam.PromptEchoOff: + return passwd, nil + case pam.PromptEchoOn, pam.ErrorMsg, pam.TextInfo: + return "", nil + } + return "", errors.New("Unrecognized PAM message style") + }) + + if err != nil { + return err + } + + if err = t.Authenticate(0); err != nil { + return err + } + + return nil +} diff --git a/pkg/auth/pam/pam_stub.go b/pkg/auth/pam/pam_stub.go new file mode 100644 index 00000000..33ac751a --- /dev/null +++ b/pkg/auth/pam/pam_stub.go @@ -0,0 +1,15 @@ +// +build !pam + +// 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 + +import ( + "errors" +) + +func PAMAuth(serviceName, userName, passwd string) error { + return errors.New("PAM not supported") +} |