mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-25 00:44:15 -05:00
834d92a47b
This is feature is workaround for #2628 (JumpCloud) and some other services that allow LDAP search only under BindDN user account, but not allow any LDAP search query in logged user DN context. Such approach is an alternative to minimal permissions security pattern for BindDN user.
219 lines
6.5 KiB
Go
219 lines
6.5 KiB
Go
// 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"
|
|
|
|
"gopkg.in/ldap.v2"
|
|
|
|
"github.com/gogits/gogs/modules/log"
|
|
)
|
|
|
|
// Basic LDAP authentication service
|
|
type Source struct {
|
|
Name string // canonical name (ie. corporate.ad)
|
|
Host string // LDAP host
|
|
Port int // port number
|
|
UseSSL bool // Use SSL
|
|
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.Debug("'%s' contains invalid query characters. Aborting.", 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) {
|
|
log.Debug("'%s' contains invalid DN characters. Aborting.", 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.Debug("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 using filter %s", userFilter)
|
|
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.Debug("Failed search using filter[%s]: %v", userFilter, err)
|
|
return "", false
|
|
} else if len(sr.Entries) > 1 {
|
|
log.Debug("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
|
|
}
|
|
|
|
// 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) {
|
|
l, err := ldapDial(ls)
|
|
if err != nil {
|
|
log.Error(4, "LDAP Connect error, %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
|
|
}
|
|
|
|
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 unexpectedly! (%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 unexpectedly! (0 entries)")
|
|
}
|
|
|
|
return "", "", "", "", false, false
|
|
}
|
|
|
|
username_attr := sr.Entries[0].GetAttributeValue(ls.AttributeUsername)
|
|
name_attr := sr.Entries[0].GetAttributeValue(ls.AttributeName)
|
|
sn_attr := sr.Entries[0].GetAttributeValue(ls.AttributeSurname)
|
|
mail_attr := sr.Entries[0].GetAttributeValue(ls.AttributeMail)
|
|
|
|
admin_attr := false
|
|
if len(ls.AdminFilter) > 0 {
|
|
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 unexpectedly! (%v)", err)
|
|
} else if len(sr.Entries) < 1 {
|
|
log.Error(4, "LDAP Admin Search failed")
|
|
} else {
|
|
admin_attr = 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_attr, name_attr, sn_attr, mail_attr, admin_attr, true
|
|
}
|
|
|
|
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.Debug("LDAP auth. failed for %s, reason: %v", userDN, err)
|
|
return err
|
|
}
|
|
log.Trace("Bound successfully with userDN: %s", userDN)
|
|
return err
|
|
}
|
|
|
|
func ldapDial(ls *Source) (*ldap.Conn, error) {
|
|
if ls.UseSSL {
|
|
log.Debug("Using TLS for LDAP without verifying: %v", ls.SkipVerify)
|
|
return ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ls.Host, ls.Port), &tls.Config{
|
|
InsecureSkipVerify: ls.SkipVerify,
|
|
})
|
|
} else {
|
|
return ldap.Dial("tcp", fmt.Sprintf("%s:%d", ls.Host, ls.Port))
|
|
}
|
|
}
|