Use client provided credentials to operated LDAP queries
This commit is contained in:
parent
c4e980834d
commit
f8a8c1a0c8
@ -1,9 +1,11 @@
|
|||||||
LISTEN="0.0.0.0:8081"
|
LISTEN="0.0.0.0:8080"
|
||||||
LDAP_HOST="ldap://ldap.example.org"
|
LDAP_HOST="ldap://ldap.example.org"
|
||||||
LDAP_BASE_DN="dc=example,dc=org"
|
LDAP_BASE_DN="dc=example,dc=org"
|
||||||
|
|
||||||
# Credentials used for every op in ldap, so this account needs write access if you want to update ldap
|
# This account search for valid user provided by authenticating client.
|
||||||
LDAP_USER="cn=ldapuser,dc=example,dc=org"
|
# Then glapi bind with client provided credentials to operate LDAP.
|
||||||
|
# Thus this account only needs bind privilege, and read access to users organizational unit
|
||||||
|
LDAP_USER="cn=ldapreaduser,dc=example,dc=org"
|
||||||
LDAP_PASS='here_lies_the_password'
|
LDAP_PASS='here_lies_the_password'
|
||||||
|
|
||||||
# Https support
|
# Https support
|
||||||
|
22
ldap.go
22
ldap.go
@ -31,13 +31,13 @@ func connectLdap(myldap *MyLdap) (*MyLdap, error) {
|
|||||||
|
|
||||||
myldap.Conn, err = ldap.DialURL(myldap.Host)
|
myldap.Conn, err = ldap.DialURL(myldap.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Error dialing LDAP: %v", err)
|
//log.Errorf("Error dialing LDAP: %v", err)
|
||||||
return conLdap, err
|
return conLdap, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = myldap.Conn.Bind(myldap.User, myldap.Pass)
|
err = myldap.Conn.Bind(myldap.User, myldap.Pass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Error binding LDAP: ", err)
|
//log.Errorf("Error binding LDAP: %v", err)
|
||||||
return conLdap, err
|
return conLdap, err
|
||||||
}
|
}
|
||||||
return myldap, err
|
return myldap, err
|
||||||
@ -91,7 +91,7 @@ func doLdapSearch(myldap *MyLdap, baseDn, cn, class, attributes string) (*ldap.S
|
|||||||
log.Debugf("LDAP search filter: %s", filter)
|
log.Debugf("LDAP search filter: %s", filter)
|
||||||
|
|
||||||
// Build absolute search base DN from configuration & provided DN (which is relative)
|
// Build absolute search base DN from configuration & provided DN (which is relative)
|
||||||
if strings.EqualFold(baseDn, "ALL") {
|
if strings.EqualFold(baseDn, "ALL") || len(baseDn) == 0 {
|
||||||
realBaseDn = fmt.Sprintf("%s", myldap.BaseDN)
|
realBaseDn = fmt.Sprintf("%s", myldap.BaseDN)
|
||||||
} else {
|
} else {
|
||||||
realBaseDn = fmt.Sprintf("%s,%s", baseDn, myldap.BaseDN)
|
realBaseDn = fmt.Sprintf("%s,%s", baseDn, myldap.BaseDN)
|
||||||
@ -118,3 +118,19 @@ func doLdapSearch(myldap *MyLdap, baseDn, cn, class, attributes string) (*ldap.S
|
|||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findUserFullDN(myldap *MyLdap, username string) (string, error) {
|
||||||
|
sr, err := doLdapSearch(myldap, "", username, "ALL", "")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(sr.Entries) == 0 {
|
||||||
|
return "", fmt.Errorf("User not found with cn=%s", username)
|
||||||
|
} else if len(sr.Entries) > 1 {
|
||||||
|
return "", fmt.Errorf("More than one object (%d) found with cn=%s", len(sr.Entries), username)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := sr.Entries[0].DN
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
158
main.go
158
main.go
@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"flag"
|
"flag"
|
||||||
"time"
|
"time"
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"net/http"
|
"net/http"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@ -20,7 +21,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gVersion = "0.5.1"
|
gVersion = "0.5.2"
|
||||||
|
|
||||||
|
gRoLdap *MyLdap
|
||||||
)
|
)
|
||||||
|
|
||||||
func marshalResultToText(res *ldap.SearchResult, delimiter string, showValueName, showDN bool) string {
|
func marshalResultToText(res *ldap.SearchResult, delimiter string, showValueName, showDN bool) string {
|
||||||
@ -143,8 +146,7 @@ func checkIfModifiedSince(c *gin.Context, myldap *MyLdap, baseDn, cn, class, att
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Basic Authentication handler
|
// Basic Authentication handler with local hardcoded account - do not use
|
||||||
// TODO: Where to store accounts?
|
|
||||||
func basicAuth(c *gin.Context) {
|
func basicAuth(c *gin.Context) {
|
||||||
user, password, hasAuth := c.Request.BasicAuth()
|
user, password, hasAuth := c.Request.BasicAuth()
|
||||||
if hasAuth && user == "admin" && password == "admin" {
|
if hasAuth && user == "admin" && password == "admin" {
|
||||||
@ -156,52 +158,121 @@ func basicAuth(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Basic Authentication handler to ldap
|
||||||
|
func ldapBasicAuth(c *gin.Context) {
|
||||||
|
var err error
|
||||||
|
// Get user & password from http client
|
||||||
|
user, password, hasAuth := c.Request.BasicAuth()
|
||||||
|
|
||||||
|
if hasAuth {
|
||||||
|
// First find the full DN for provided username
|
||||||
|
if gRoLdap.Conn == nil {
|
||||||
|
gRoLdap, err = connectLdap(gRoLdap)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("[%s]: Cannot connect to LDAP: %v", c.Request.RemoteAddr, err)
|
||||||
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
|
c.Writer.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userDn, err := findUserFullDN(gRoLdap, user)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("[%s]: Cannot connect to LDAP: %v", c.Request.RemoteAddr, err)
|
||||||
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
|
c.Writer.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then open this ldap connection bound to client credentials, using found full Dn
|
||||||
|
cl, err := connectLdap(&MyLdap{Host: gRoLdap.Host, User: userDn, Pass: password, BaseDN: gRoLdap.BaseDN})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("[%s]: Cannot connect to LDAP: %v", c.Request.RemoteAddr, err)
|
||||||
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
|
c.Writer.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
log.Infof("[%s]: User %s successfully authenticated with bind DN %s", c.Request.RemoteAddr, user, userDn)
|
||||||
|
// Store LDAP connection into gin context
|
||||||
|
c.Set("ldapCon", cl)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
|
c.Writer.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func initRouter(r *gin.Engine, myldap *MyLdap) {
|
|
||||||
|
func getLdapConFromContext(c *gin.Context) (*MyLdap, error) {
|
||||||
|
ldapCon, exist := c.Get("ldapCon")
|
||||||
|
if exist != true {
|
||||||
|
return nil, errors.New("Cannot get connection from context")
|
||||||
|
}
|
||||||
|
return ldapCon.(*MyLdap), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func initRouter(r *gin.Engine) {
|
||||||
r.GET("/ping", func(c *gin.Context) {
|
r.GET("/ping", func(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"message": "pong",
|
"message": "pong",
|
||||||
})
|
})
|
||||||
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
// All following routes need authentication
|
// All following routes need authentication
|
||||||
r.GET("/:ou/:cn/:class", basicAuth, func(c *gin.Context) {
|
r.GET("/:ou/:cn/:class", ldapBasicAuth, func(c *gin.Context) {
|
||||||
ou := c.Param("ou")
|
ou := c.Param("ou")
|
||||||
cn := c.Param("cn")
|
cn := c.Param("cn")
|
||||||
class := c.Param("class")
|
class := c.Param("class")
|
||||||
|
|
||||||
|
// Get user authenticated LDAP connection from context
|
||||||
|
ldapCon, err := getLdapConFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// json format is the default
|
// json format is the default
|
||||||
format := c.DefaultQuery("format", "json")
|
format := c.DefaultQuery("format", "json")
|
||||||
log.Printf("Format : %s", format)
|
|
||||||
|
res, err := doLdapSearch(ldapCon, ou, cn, class, "ALL")
|
||||||
res, err := doLdapSearch(myldap, ou, cn, class, "ALL")
|
|
||||||
|
// If OU does not exist, we'll get err='LDAP Result Code 32 "No Such Object"'
|
||||||
// If OU does not exist, we'll get err='LDAP Result Code 32 "No Such Object"'
|
if err != nil {
|
||||||
if err != nil {
|
log.Errorf("Error searching %s in %s : %v", cn, ou, err)
|
||||||
log.Errorf("Error searching %s in %s : %v", cn, ou, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sendResponse(c, res, format)
|
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
sendResponse(c, res, format)
|
||||||
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
r.HEAD("/:ou/:cn/:class", basicAuth, func(c *gin.Context) {
|
r.HEAD("/:ou/:cn/:class", ldapBasicAuth, func(c *gin.Context) {
|
||||||
ou := c.Param("ou")
|
ou := c.Param("ou")
|
||||||
cn := c.Param("cn")
|
cn := c.Param("cn")
|
||||||
class := c.Param("class")
|
class := c.Param("class")
|
||||||
|
|
||||||
format := c.DefaultQuery("format", "json")
|
format := c.DefaultQuery("format", "json")
|
||||||
log.Printf("Format : %s", format)
|
|
||||||
|
|
||||||
modified, err := checkIfModifiedSince(c, myldap, ou, cn, class, "ALL")
|
// Get user authenticated LDAP connection from context
|
||||||
|
ldapCon, err := getLdapConFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
modified, err := checkIfModifiedSince(c, ldapCon, ou, cn, class, "ALL")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if modified {
|
if modified {
|
||||||
res, err := doLdapSearch(myldap, ou, cn, class, "ALL")
|
res, err := doLdapSearch(ldapCon, ou, cn, class, "ALL")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Error searching %s in %s : %v", cn, ou, err)
|
log.Errorf("Error searching %s in %s : %v", cn, ou, err)
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
@ -214,16 +285,23 @@ func initRouter(r *gin.Engine, myldap *MyLdap) {
|
|||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
r.GET("/:ou/:cn/:class/:attribute", basicAuth, func(c *gin.Context) {
|
r.GET("/:ou/:cn/:class/:attribute", ldapBasicAuth, func(c *gin.Context) {
|
||||||
ou := c.Param("ou")
|
ou := c.Param("ou")
|
||||||
cn := c.Param("cn")
|
cn := c.Param("cn")
|
||||||
attr := c.Param("attribute")
|
attr := c.Param("attribute")
|
||||||
class := c.Param("class")
|
class := c.Param("class")
|
||||||
|
|
||||||
format := c.DefaultQuery("format", "json")
|
format := c.DefaultQuery("format", "json")
|
||||||
log.Printf("Format : %s", format)
|
|
||||||
|
// Get user authenticated LDAP connection from context
|
||||||
|
ldapCon, err := getLdapConFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
res, err := doLdapSearch(myldap, ou, cn, class, attr)
|
res, err := doLdapSearch(ldapCon, ou, cn, class, attr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Error searching %s in %s : %v", cn, ou, err)
|
log.Errorf("Error searching %s in %s : %v", cn, ou, err)
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
@ -233,23 +311,30 @@ func initRouter(r *gin.Engine, myldap *MyLdap) {
|
|||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
r.HEAD("/:ou/:cn/:class/:attribute", basicAuth, func(c *gin.Context) {
|
r.HEAD("/:ou/:cn/:class/:attribute", ldapBasicAuth, func(c *gin.Context) {
|
||||||
ou := c.Param("ou")
|
ou := c.Param("ou")
|
||||||
cn := c.Param("cn")
|
cn := c.Param("cn")
|
||||||
attr := c.Param("attribute")
|
attr := c.Param("attribute")
|
||||||
class := c.Param("class")
|
class := c.Param("class")
|
||||||
|
|
||||||
format := c.DefaultQuery("format", "json")
|
format := c.DefaultQuery("format", "json")
|
||||||
log.Printf("Format : %s", format)
|
|
||||||
|
|
||||||
modified, err := checkIfModifiedSince(c, myldap, ou, cn, class, attr)
|
// Get user authenticated LDAP connection from context
|
||||||
|
ldapCon, err := getLdapConFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
modified, err := checkIfModifiedSince(c, ldapCon, ou, cn, class, attr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if modified {
|
if modified {
|
||||||
res, err := doLdapSearch(myldap, ou, cn, class, attr)
|
res, err := doLdapSearch(ldapCon, ou, cn, class, attr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Error searching %s in %s : %v", cn, ou, err)
|
log.Errorf("Error searching %s in %s : %v", cn, ou, err)
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
@ -278,7 +363,7 @@ func main() {
|
|||||||
flag.StringVar(&confFile, "config", "", "Path to the config file (optional)")
|
flag.StringVar(&confFile, "config", "", "Path to the config file (optional)")
|
||||||
flag.StringVar(&listen, "listen-addr", "0.0.0.0:8080", "listen address for server")
|
flag.StringVar(&listen, "listen-addr", "0.0.0.0:8080", "listen address for server")
|
||||||
flag.StringVar(&ldapHost, "ldap-host", "", "ldap host to connect to")
|
flag.StringVar(&ldapHost, "ldap-host", "", "ldap host to connect to")
|
||||||
flag.StringVar(&ldapUser, "ldap-user", "", "ldap username")
|
flag.StringVar(&ldapUser, "ldap-user", "", "ldap read-only username")
|
||||||
flag.StringVar(&ldapPass, "ldap-pass", "", "ldap password")
|
flag.StringVar(&ldapPass, "ldap-pass", "", "ldap password")
|
||||||
flag.StringVar(&ldapBaseDN, "ldap-base-dn", "", "ldap base DN")
|
flag.StringVar(&ldapBaseDN, "ldap-base-dn", "", "ldap base DN")
|
||||||
flag.BoolVar(&doTls, "https", false, "Serve over TLS")
|
flag.BoolVar(&doTls, "https", false, "Serve over TLS")
|
||||||
@ -295,6 +380,10 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if false == debug {
|
||||||
|
debug = viper.GetBool("DEBUG")
|
||||||
|
}
|
||||||
|
|
||||||
if strings.EqualFold(listen, "0.0.0.0:8080") && len(confFile) > 0 {
|
if strings.EqualFold(listen, "0.0.0.0:8080") && len(confFile) > 0 {
|
||||||
l := viper.GetString("LISTEN")
|
l := viper.GetString("LISTEN")
|
||||||
@ -362,9 +451,14 @@ func main() {
|
|||||||
|
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
ldap := MyLdap{Host: ldapHost, User: ldapUser, Pass: ldapPass, BaseDN: ldapBaseDN}
|
gRoLdap = &MyLdap{Host: ldapHost, User: ldapUser, Pass: ldapPass, BaseDN: ldapBaseDN}
|
||||||
|
_, err := connectLdap(gRoLdap)
|
||||||
initRouter(r, &ldap)
|
if err != nil {
|
||||||
|
log.Fatalf("Cannot connect to ldap: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
initRouter(r)
|
||||||
|
|
||||||
if doTls {
|
if doTls {
|
||||||
r.RunTLS(listen, tlsCert, tlsPrivKey)
|
r.RunTLS(listen, tlsCert, tlsPrivKey)
|
||||||
|
Loading…
Reference in New Issue
Block a user