From f8a8c1a0c826e7c4b693407881a16b63e75b02f3 Mon Sep 17 00:00:00 2001 From: yo Date: Sat, 12 Nov 2022 14:00:06 +0100 Subject: [PATCH] Use client provided credentials to operated LDAP queries --- glapi.env.sample | 8 ++- ldap.go | 22 ++++++- main.go | 158 +++++++++++++++++++++++++++++++++++++---------- 3 files changed, 150 insertions(+), 38 deletions(-) diff --git a/glapi.env.sample b/glapi.env.sample index 547eb58..732e747 100644 --- a/glapi.env.sample +++ b/glapi.env.sample @@ -1,9 +1,11 @@ -LISTEN="0.0.0.0:8081" +LISTEN="0.0.0.0:8080" LDAP_HOST="ldap://ldap.example.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 -LDAP_USER="cn=ldapuser,dc=example,dc=org" +# This account search for valid user provided by authenticating client. +# 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' # Https support diff --git a/ldap.go b/ldap.go index ae1a831..e68e078 100644 --- a/ldap.go +++ b/ldap.go @@ -31,13 +31,13 @@ func connectLdap(myldap *MyLdap) (*MyLdap, error) { myldap.Conn, err = ldap.DialURL(myldap.Host) if err != nil { - log.Errorf("Error dialing LDAP: %v", err) + //log.Errorf("Error dialing LDAP: %v", err) return conLdap, err } err = myldap.Conn.Bind(myldap.User, myldap.Pass) if err != nil { - log.Errorf("Error binding LDAP: ", err) + //log.Errorf("Error binding LDAP: %v", err) return conLdap, 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) // 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) } else { 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 } + +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 +} diff --git a/main.go b/main.go index 45f3918..fa17f67 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "fmt" "flag" "time" + "errors" "strings" "net/http" "encoding/json" @@ -20,7 +21,9 @@ import ( ) var ( - gVersion = "0.5.1" + gVersion = "0.5.2" + + gRoLdap *MyLdap ) 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 -// TODO: Where to store accounts? +// Basic Authentication handler with local hardcoded account - do not use func basicAuth(c *gin.Context) { user, password, hasAuth := c.Request.BasicAuth() 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) { c.JSON(http.StatusOK, gin.H{ "message": "pong", }) + return }) // 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") cn := c.Param("cn") 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 format := c.DefaultQuery("format", "json") - log.Printf("Format : %s", format) - - 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 err != nil { - log.Errorf("Error searching %s in %s : %v", cn, ou, err) - c.AbortWithError(http.StatusInternalServerError, err) - return - } - sendResponse(c, res, format) + + res, err := doLdapSearch(ldapCon, ou, cn, class, "ALL") + + // If OU does not exist, we'll get err='LDAP Result Code 32 "No Such Object"' + if err != nil { + log.Errorf("Error searching %s in %s : %v", cn, ou, err) + c.AbortWithError(http.StatusInternalServerError, err) 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") cn := c.Param("cn") class := c.Param("class") - + 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 { c.AbortWithError(http.StatusInternalServerError, err) return } if modified { - res, err := doLdapSearch(myldap, ou, cn, class, "ALL") + res, err := doLdapSearch(ldapCon, ou, cn, class, "ALL") if err != nil { log.Errorf("Error searching %s in %s : %v", cn, ou, err) c.AbortWithError(http.StatusInternalServerError, err) @@ -214,16 +285,23 @@ func initRouter(r *gin.Engine, myldap *MyLdap) { 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") cn := c.Param("cn") attr := c.Param("attribute") class := c.Param("class") 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 { log.Errorf("Error searching %s in %s : %v", cn, ou, err) c.AbortWithError(http.StatusInternalServerError, err) @@ -233,23 +311,30 @@ func initRouter(r *gin.Engine, myldap *MyLdap) { 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") cn := c.Param("cn") attr := c.Param("attribute") class := c.Param("class") 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 { c.AbortWithError(http.StatusInternalServerError, err) return } if modified { - res, err := doLdapSearch(myldap, ou, cn, class, attr) + res, err := doLdapSearch(ldapCon, ou, cn, class, attr) if err != nil { log.Errorf("Error searching %s in %s : %v", cn, ou, err) c.AbortWithError(http.StatusInternalServerError, err) @@ -278,7 +363,7 @@ func main() { 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(&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(&ldapBaseDN, "ldap-base-dn", "", "ldap base DN") flag.BoolVar(&doTls, "https", false, "Serve over TLS") @@ -295,6 +380,10 @@ func main() { os.Exit(1) } } + + if false == debug { + debug = viper.GetBool("DEBUG") + } if strings.EqualFold(listen, "0.0.0.0:8080") && len(confFile) > 0 { l := viper.GetString("LISTEN") @@ -362,9 +451,14 @@ func main() { r := gin.Default() - ldap := MyLdap{Host: ldapHost, User: ldapUser, Pass: ldapPass, BaseDN: ldapBaseDN} - - initRouter(r, &ldap) + gRoLdap = &MyLdap{Host: ldapHost, User: ldapUser, Pass: ldapPass, BaseDN: ldapBaseDN} + _, err := connectLdap(gRoLdap) + if err != nil { + log.Fatalf("Cannot connect to ldap: %v", err) + } + + + initRouter(r) if doTls { r.RunTLS(listen, tlsCert, tlsPrivKey)