5 Commits
v0.4 ... v0.5.2

Author SHA1 Message Date
yo
4a8edd3ebd Update readme 2022-11-12 14:05:14 +01:00
yo
f8a8c1a0c8 Use client provided credentials to operated LDAP queries 2022-11-12 14:00:06 +01:00
yo
c4e980834d Add TLS/https options 2022-11-11 13:01:33 +01:00
yo
5e363df9b0 Use builtin status codes, change HTTPS option 2022-11-11 13:00:57 +01:00
yo
bdda2de936 https support 2022-10-09 09:16:23 +02:00
4 changed files with 245 additions and 87 deletions

View File

@ -16,11 +16,20 @@ glapi -config glapi.env
## Configuration file ## Configuration file
``` ```
LISTEN="127.0.0.1:8080" 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"
LDAP_USER="cn=yo,dc=example,dc=org"
LDAP_PASS='here_is_the_password' # 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
HTTPS=true
SSL_CERTIFICATE=/etc/ssl/certs/server.pem
SSL_PRIVATE_KEY=/etc/ssl/private/server.key
``` ```
## Querying API ## Querying API

View File

@ -1,5 +1,14 @@
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"
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' LDAP_PASS='here_lies_the_password'
# Https support
HTTPS=false
SSL_CERTIFICATE=/etc/ssl/certs/server.pem
SSL_PRIVATE_KEY=/etc/ssl/private/server.key

22
ldap.go
View File

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

282
main.go
View File

@ -9,7 +9,9 @@ import (
"fmt" "fmt"
"flag" "flag"
"time" "time"
"errors"
"strings" "strings"
"net/http"
"encoding/json" "encoding/json"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -19,7 +21,9 @@ import (
) )
var ( var (
gVersion = "0.4" 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 {
@ -54,9 +58,9 @@ func sendResponse(c *gin.Context, res *ldap.SearchResult, format string) {
// 404 Not found // 404 Not found
if len(res.Entries) == 0 { if len(res.Entries) == 0 {
if strings.EqualFold(format, "json") { if strings.EqualFold(format, "json") {
c.JSON(404, gin.H{"error": "No result"}) c.JSON(http.StatusNotFound, gin.H{"error": "No result"})
} else { } else {
c.String(404, "No result") c.String(http.StatusNotFound, "No result")
} }
return return
} }
@ -69,27 +73,27 @@ func sendResponse(c *gin.Context, res *ldap.SearchResult, format string) {
log.Errorf("Error marshalling result to json: %v", err) log.Errorf("Error marshalling result to json: %v", err)
} }
log.Debugf("%v\n", string(jsonRes)) log.Debugf("%v\n", string(jsonRes))
c.String(200, string(jsonRes)) c.String(http.StatusOK, string(jsonRes))
} else if strings.EqualFold(format, "text") { } else if strings.EqualFold(format, "text") {
txtRes := marshalResultToText(res, "=", false, true) txtRes := marshalResultToText(res, "=", false, true)
log.Debugf("%v\n", string(txtRes)) log.Debugf("%v\n", string(txtRes))
c.String(200, string(txtRes)) c.String(http.StatusOK, string(txtRes))
} else if strings.EqualFold(format, "ldif") { } else if strings.EqualFold(format, "ldif") {
txtRes := marshalResultToText(res, ": ", false, true) txtRes := marshalResultToText(res, ": ", false, true)
log.Debugf("%v\n", string(txtRes)) log.Debugf("%v\n", string(txtRes))
c.String(200, string(txtRes)) c.String(http.StatusOK, string(txtRes))
} else if strings.EqualFold(format, "textvalue") { } else if strings.EqualFold(format, "textvalue") {
txtRes := marshalResultToText(res, "", true, true) txtRes := marshalResultToText(res, "", true, true)
log.Debugf("%v\n", string(txtRes)) log.Debugf("%v\n", string(txtRes))
c.String(200, string(txtRes)) c.String(http.StatusOK, string(txtRes))
} else if strings.EqualFold(format, "textvalue-nodn") { } else if strings.EqualFold(format, "textvalue-nodn") {
txtRes := marshalResultToText(res, "", true, false) txtRes := marshalResultToText(res, "", true, false)
log.Debugf("%v\n", string(txtRes)) log.Debugf("%v\n", string(txtRes))
c.String(200, string(txtRes)) c.String(http.StatusOK, string(txtRes))
} }
} }
@ -142,121 +146,203 @@ 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" {
log.Infof("[%s]: User %s successfully authenticated", c.Request.RemoteAddr, user) log.Infof("[%s]: User %s successfully authenticated", c.Request.RemoteAddr, user)
} else { } else {
c.AbortWithStatus(401) c.AbortWithStatus(http.StatusUnauthorized)
c.Writer.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
return
}
}
// 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") c.Writer.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
return 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(200, 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(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(500, err)
return
}
sendResponse(c, res, format)
return
})
r.HEAD("/:ou/:cn/:class", basicAuth, func(c *gin.Context) {
ou := c.Param("ou")
cn := c.Param("cn")
class := c.Param("class")
format := c.DefaultQuery("format", "json") res, err := doLdapSearch(ldapCon, ou, cn, class, "ALL")
log.Printf("Format : %s", format)
// If OU does not exist, we'll get err='LDAP Result Code 32 "No Such Object"'
modified, err := checkIfModifiedSince(c, myldap, ou, cn, class, "ALL")
if err != nil {
c.AbortWithError(500, err)
return
}
if modified {
res, err := doLdapSearch(myldap, ou, cn, class, "ALL")
if err != nil {
log.Errorf("Error searching %s in %s : %v", cn, ou, err)
c.AbortWithError(500, err)
return
}
sendResponse(c, res, format)
} else {
c.String(304, "")
}
return
})
r.GET("/:ou/:cn/:class/:attribute", basicAuth, 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)
res, err := doLdapSearch(myldap, 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(500, err) c.AbortWithError(http.StatusInternalServerError, err)
return return
} }
sendResponse(c, res, format) sendResponse(c, res, format)
return return
}) })
r.HEAD("/:ou/:cn/:class/:attribute", 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")
// 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(ldapCon, ou, cn, class, "ALL")
if err != nil {
log.Errorf("Error searching %s in %s : %v", cn, ou, err)
c.AbortWithError(http.StatusInternalServerError, err)
return
}
sendResponse(c, res, format)
} else {
c.String(http.StatusNotModified, "")
}
return
})
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
modified, err := checkIfModifiedSince(c, myldap, ou, cn, class, attr) ldapCon, err := getLdapConFromContext(c)
if err != nil { if err != nil {
c.AbortWithError(500, err) log.Error(err)
c.AbortWithError(http.StatusInternalServerError, nil)
return
}
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)
return
}
sendResponse(c, res, format)
return
})
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")
// 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 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(500, err) c.AbortWithError(http.StatusInternalServerError, err)
return return
} }
sendResponse(c, res, format) sendResponse(c, res, format)
} else { } else {
c.String(304, "") c.String(http.StatusNotModified, "")
} }
return return
}) })
@ -269,14 +355,20 @@ func main() {
var ldapUser string var ldapUser string
var ldapPass string var ldapPass string
var ldapBaseDN string var ldapBaseDN string
var tlsPrivKey string
var tlsCert string
var doTls bool
var debug bool var debug bool
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.StringVar(&tlsPrivKey, "ssl-private-key", "", "SSL Private key")
flag.StringVar(&tlsCert, "ssl-certificate", "", "SSL certificate (PEM format)")
flag.BoolVar(&debug, "debug", false, "Set log level to debug") flag.BoolVar(&debug, "debug", false, "Set log level to debug")
flag.Parse() flag.Parse()
@ -288,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")
@ -328,6 +424,25 @@ func main() {
log.Fatal("No ldap-base-dn defined!") log.Fatal("No ldap-base-dn defined!")
} }
} }
if false == doTls {
doTls = viper.GetBool("HTTPS")
}
if doTls && len(tlsCert) == 0 {
l := viper.GetString("SSL_CERTIFICATE")
if len(l) > 0 {
tlsCert = l
} else {
log.Fatal("SSL certificate must be set to use https!")
}
}
if doTls && len(tlsPrivKey) == 0 {
l := viper.GetString("SSL_PRIVATE_KEY")
if len(l) > 0 {
tlsPrivKey = l
} else {
log.Fatal("SSL private key must be set to use https!")
}
}
log.Println("Starting Go Ldap API v.", gVersion) log.Println("Starting Go Ldap API v.", gVersion)
if debug { if debug {
@ -336,11 +451,20 @@ 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)
if err != nil {
log.Fatalf("Cannot connect to ldap: %v", err)
}
initRouter(r)
initRouter(r, &ldap) if doTls {
r.RunTLS(listen, tlsCert, tlsPrivKey)
r.Run(listen) } else {
r.Run(listen)
}
} }