15 Commits
v0.5 ... v0.5.5

4 changed files with 656 additions and 109 deletions

View File

@ -1,12 +1,13 @@
# glapi
Go Ldap API is an HTTP API to an LDAP backend.
Only supporting read at the moment, maybe one day it will write, and break LDAP backends with ease and joy!
Only supporting read at the moment, maybe one day it will write, and break LDAP backends with ease and joy!
UPDATE: Yay, the day has come!
## Usage
Start glapi with parameters on command line:
```
glapi -ldap-host ldap://ldap.example.org -ldap-base-dn dc=example,dc=org -ldap-user cn=yo,dc=example,dc=org -ldap-pass 'here_is_the_password'
glapi -ldap-host ldap://ldap.example.org -ldap-base-dn ou=configurations,dc=example,dc=org -ldap-auth-base-dn ou=users,dc=example,dc=org -ldap-user cn=yo,dc=example,dc=org -ldap-pass 'here_is_the_password'
```
or point it to a configuration file :
@ -16,16 +17,31 @@ glapi -config glapi.env
## Configuration file
```
LISTEN="127.0.0.1:8080"
LISTEN="0.0.0.0:8443"
LDAP_HOST="ldap://ldap.example.org"
# The base DN exposed to API. Could be buried in LDAP tree so we expose only a subset of directory.
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'
# This base DN is where we seach for authenticating accounts. This way we can chose not to expose them to the API.
LDAP_AUTH_BASE_DN="ou=users,dc=example,dc=org"
# Https support
HTTPS=true
SSL_CERTIFICATE=/etc/ssl/certs/server.pem
SSL_PRIVATE_KEY=/etc/ssl/private/server.key
```
## Querying API
### Search Entries
Search LDAP entries through the whole subtree, specifying organizationalUnit, commnName, objectClass, attribute to retrieve.
Each of these parameters can be replaced by "ALL" to act like a wildcard.
Each of these parameters can be replaced by "ALL" to act like a wildcard.
"admin" user should exist in LDAP_AUTH_BASE_DN and searchable with cn=admin
```
% curl -u admin:admin http://127.0.0.1:8080/ou=domains/yo/person | jq
[
@ -166,7 +182,7 @@ hasSubordinates: FALSE
[...]
```
### Output format
#### Output format
Default output is in json. The following formats are supported:
- json (default)
- text (ini style)
@ -206,7 +222,7 @@ uid: yo
```
### Select attributes to get
#### Select attributes to get
You can select attributes to get by adding them in 4th position :
```
% curl -u admin:admin "http://127.0.0.1:8080/ou=domains/yo/person/mail?format=textvalue"
@ -216,3 +232,31 @@ yo@example.org
% curl -u admin:admin "http://127.0.0.1:8080/ou=domains/yo/person/mail?format=textvalue-nodn"
yo@example.org
```
### Create entries
Create a new OU =users", then a new user :
```
% curl -u "admin:admin" --header "Content-Type: application/json" -X POST
--data '{"objectClass":["organizationalUnit","top"],"ou":"users"' \
https://127.0.0.1:8443/ou=users,dc=example,dc=org
% curl -u "admin:admin" --header "Content-Type: application/json" -X POST
--data '{"objectClass":["person","top"],"cn":"newuser","sn":"New"}' \
https://127.0.0.1:8443/cn=newuser,ou=users,dc=example,dc=org
```
### Modify entries
Add a description to the new account:
```
% curl -u "admin:admin" --header "Content-Type: application/json" -X PUT
--data '{"objectClass":["person","top"],"cn":"newuser","sn":"New","description":"Test account"}' \
https://127.0.0.1:8443/cn=newuser,ou=users,dc=example,dc=org
```
Missing attributes will be removed from entry.
### Delete entries
Remove newuser :
```
% curl -u "admin:admin" -X DELETE \
https://127.0.0.1:8443/cn=newuser,ou=users,dc=example,dc=org
```

View File

@ -1,5 +1,18 @@
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"
LDAP_USER="cn=ldapuser,dc=example,dc=org"
# The base DN exposed to API. Could be buried in LDAP tree so we expose only a subset of directory.
LDAP_BASE_DN="ou=configuration,dc=example,dc=org"
# This account search for valid users provided by authenticating clients.
# 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'
# This base DN is where we seach for authenticating accounts. This way we can chose not to expose them to the API.
LDAP_AUTH_BASE_DN="ou=users,dc=example,dc=org"
# Https support
HTTPS=false
SSL_CERTIFICATE=/etc/ssl/certs/server.pem
SSL_PRIVATE_KEY=/etc/ssl/private/server.key

205
ldap.go
View File

@ -19,31 +19,54 @@ type MyLdap struct {
User string
Pass string
BaseDN string
AuthBaseDN string
}
var (
mutex sync.Mutex
)
func marshalResultToStrMap(sr *ldap.SearchResult) map[string][]string {
var res = make(map[string][]string)
for _, e := range sr.Entries {
for _, a := range e.Attributes {
for _, v := range a.Values {
res[a.Name] = append(res[a.Name], v)
}
}
}
return res
}
func isStringInArray(strarr []string, searched string) bool {
for _, s := range strarr {
if strings.EqualFold(s, searched) {
return true
}
}
return false
}
func connectLdap(myldap *MyLdap) (*MyLdap, error) {
var err error
var conLdap *MyLdap
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
}
func ldapSearch(conLdap *MyLdap, searchReq *ldap.SearchRequest, attempt int) (*ldap.SearchResult, error) {
func internalLdapSearch(conLdap *MyLdap, searchReq *ldap.SearchRequest, attempt int) (*ldap.SearchResult, error) {
var err error
if conLdap.Conn == nil {
conLdap, err = connectLdap(conLdap)
@ -65,36 +88,59 @@ func ldapSearch(conLdap *MyLdap, searchReq *ldap.SearchRequest, attempt int) (*l
return result, err
} else {
attempt = attempt + 1
return ldapSearch(conLdap, searchReq, attempt)
return internalLdapSearch(conLdap, searchReq, attempt)
}
}
return result, err
}
func doLdapSearch(myldap *MyLdap, baseDn, cn, class, attributes string) (*ldap.SearchResult, error) {
func searchByCn(myldap *MyLdap, baseDn, cn, class, attributes string) (*ldap.SearchResult, error) {
var filter string
if false == strings.EqualFold(cn, "ALL") {
filter = fmt.Sprintf("(cn=%s)", cn)
} else {
filter = cn
}
return doLdapSearch(myldap, baseDn, false, filter, class, attributes)
}
func searchByDn(myldap *MyLdap, dn, attributes string) (*ldap.SearchResult, error) {
// We cant search for a full DN, so separate cn and base dn, then remove basedn as doLdapSearch already append it
filter := strings.Split(dn, ",")[0]
rem := strings.Split(dn, ",")[1:]
bdn := strings.Join(rem, ",")
bdn = strings.Replace(bdn, fmt.Sprintf(",%s", myldap.BaseDN), "", 1)
return doLdapSearch(myldap, bdn, false, filter, "ALL", "ALL")
}
func doLdapSearch(myldap *MyLdap, baseDn string, baseDnIsAbsolute bool, filter, class, attributes string) (*ldap.SearchResult, error) {
var fFilter string
var realBaseDn string
var realAttributes []string
// Build search filter
// Build final search filter
if strings.EqualFold(class, "ALL") {
filter = fmt.Sprintf("(objectClass=*)")
fFilter = fmt.Sprintf("(objectClass=*)")
} else {
filter = fmt.Sprintf("(objectClass=%s)", class)
fFilter = fmt.Sprintf("(objectClass=%s)", class)
}
if false == strings.EqualFold(cn, "ALL") {
filter = fmt.Sprintf("(&%s(cn=%s))", filter, ldap.EscapeFilter(cn))
if false == strings.EqualFold(filter, "ALL") {
fFilter = fmt.Sprintf("(&%s(%s))", fFilter, ldap.EscapeFilter(filter))
}
log.Debugf("LDAP search filter: %s", filter)
log.Debugf("LDAP search filter: %s", fFilter)
// 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)
if len(baseDn) > 0 && baseDnIsAbsolute {
realBaseDn = fmt.Sprintf("%s", baseDn)
} else {
realBaseDn = fmt.Sprintf("%s,%s", baseDn, myldap.BaseDN)
}
}
log.Debugf("LDAP search base dn: %s", realBaseDn)
@ -109,12 +155,141 @@ func doLdapSearch(myldap *MyLdap, baseDn, cn, class, attributes string) (*ldap.S
log.Debugf("LDAP search attributes to return (all if empty): %v", realAttributes)
searchReq := ldap.NewSearchRequest(realBaseDn, ldap.ScopeWholeSubtree, 0, 0, 0,
false, filter, realAttributes, []ldap.Control{})
false, fFilter, realAttributes, []ldap.Control{})
result, err := ldapSearch(myldap, searchReq, 0)
result, err := internalLdapSearch(myldap, searchReq, 0)
if err != nil {
return nil, err
}
return result, nil
}
func findUserFullDN(myldap *MyLdap, username string) (string, error) {
filter := fmt.Sprintf("cn=%s", username)
sr, err := doLdapSearch(myldap, myldap.AuthBaseDN, true, filter, "ALL", "")
if err != nil {
return "", err
}
if len(sr.Entries) == 0 {
return "", fmt.Errorf("User not found with %s", filter)
} else if len(sr.Entries) > 1 {
return "", fmt.Errorf("More than one object (%d) found with %s", len(sr.Entries), filter)
}
result := sr.Entries[0].DN
return result, nil
}
func createEntry(myldap *MyLdap, dn string, attributes map[string]interface{}) error {
log.Debugf("Creating DN %s with attributes %v", dn, attributes)
addReq := ldap.NewAddRequest(dn, nil)
// Remove dn from map if exists
delete(attributes, "dn")
//build attributes list
for key, val := range attributes {
var v []string
strval := fmt.Sprintf("%s",val)
if strings.HasPrefix(strval, "[") && strings.HasSuffix(strval, "]") {
for _, va := range val.([]interface{}) {
v = append(v, va.(string))
}
} else {
v = append(v, strval)
}
addReq.Attribute(key, v)
}
err := myldap.Conn.Add(addReq)
return err
}
func updateEntry(myldap *MyLdap, dn string, attributes map[string]interface{}) error {
log.Debugf("Updating DN %s with attributes %v", dn, attributes)
// Remove dn from map if exists
delete(attributes, "dn")
// First get the current object so we can build a list of add, modify, delete attributes
sr, err := searchByDn(myldap, dn, "ALL")
if err != nil {
return err
}
if len(sr.Entries) == 0 {
return fmt.Errorf("Object %s not found", dn)
}
actualAttrs := marshalResultToStrMap(sr)
// convert attributes to map[string][]string
var attrsMap = make(map[string][]string)
for key, val := range attributes {
var v []string
strval := fmt.Sprintf("%s",val)
if strings.HasPrefix(strval, "[") && strings.HasSuffix(strval, "]") {
for _, va := range val.([]interface{}) {
v = append(v, va.(string))
}
} else {
v = append(v, strval)
}
attrsMap[key] = v
}
modReq := ldap.NewModifyRequest(dn, nil)
// Now compare. We browse required attributes to get modify and add operations
opnr := 0
for k, v := range attrsMap {
fv := 0
va, found := actualAttrs[k]
if found {
for _, cv := range va {
if isStringInArray(v, cv) {
fv += 1
}
}
if fv != len(va) {
modReq.Replace(k, v)
opnr++
}
} else {
modReq.Add(k, v)
opnr++
}
}
// now browse actual attributes to get delete operations
for k, v := range actualAttrs {
_, found := attrsMap[k]
if false == found {
modReq.Delete(k, v)
opnr++
}
}
if opnr == 0 {
return fmt.Errorf("No modification required")
}
// Finally execute operations
err = myldap.Conn.Modify(modReq)
return err
}
func deleteEntry(myldap *MyLdap, dn string) error {
log.Debugf("Deleting DN %s", dn)
delReq := ldap.NewDelRequest(dn, nil)
err := myldap.Conn.Del(delReq)
return err
}

481
main.go
View File

@ -9,17 +9,22 @@ import (
"fmt"
"flag"
"time"
"errors"
"strings"
"net/http"
"encoding/json"
"github.com/spf13/viper"
"github.com/gin-gonic/gin"
"github.com/go-ldap/ldap/v3"
log "github.com/sirupsen/logrus"
//"github.com/gin-gonic/gin/render"
)
var (
gVersion = "0.5"
gVersion = "0.5.5"
gRoLdap *MyLdap
)
func marshalResultToText(res *ldap.SearchResult, delimiter string, showValueName, showDN bool) string {
@ -54,9 +59,9 @@ func sendResponse(c *gin.Context, res *ldap.SearchResult, format string) {
// 404 Not found
if len(res.Entries) == 0 {
if strings.EqualFold(format, "json") {
c.JSON(404, gin.H{"error": "No result"})
c.JSON(http.StatusNotFound, gin.H{"error": "No result"})
} else {
c.String(404, "No result")
c.String(http.StatusNotFound, "No result")
}
return
}
@ -69,28 +74,27 @@ func sendResponse(c *gin.Context, res *ldap.SearchResult, format string) {
log.Errorf("Error marshalling result to json: %v", err)
}
log.Debugf("%v\n", string(jsonRes))
c.String(200, string(jsonRes))
c.String(http.StatusOK, string(jsonRes))
} else if strings.EqualFold(format, "text") {
txtRes := marshalResultToText(res, "=", false, true)
log.Debugf("%v\n", string(txtRes))
c.String(200, string(txtRes))
c.String(http.StatusOK, string(txtRes))
} else if strings.EqualFold(format, "ldif") {
txtRes := marshalResultToText(res, ": ", false, true)
log.Debugf("%v\n", string(txtRes))
c.String(200, string(txtRes))
c.String(http.StatusOK, string(txtRes))
} else if strings.EqualFold(format, "textvalue") {
txtRes := marshalResultToText(res, "", true, true)
log.Debugf("%v\n", string(txtRes))
c.String(200, string(txtRes))
c.String(http.StatusOK, string(txtRes))
} else if strings.EqualFold(format, "textvalue-nodn") {
txtRes := marshalResultToText(res, "", true, false)
log.Debugf("%v\n", string(txtRes))
c.String(200, string(txtRes))
c.String(http.StatusOK, string(txtRes))
}
}
@ -107,7 +111,7 @@ func checkIfModifiedSince(c *gin.Context, myldap *MyLdap, baseDn, cn, class, att
log.Debugf("ifModifiedSince: %s", ifModifiedSince)
res, err := doLdapSearch(myldap, baseDn, cn, class, "modifyTimestamp")
res, err := searchByCn(myldap, baseDn, cn, class, "modifyTimestamp")
if err != nil {
log.Errorf("Error searching modifyTimestamp for %s in %s : %v", cn, baseDn, err)
return true, err
@ -141,125 +145,417 @@ func checkIfModifiedSince(c *gin.Context, myldap *MyLdap, baseDn, cn, class, att
return true, nil
}
// 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" {
log.Infof("[%s]: User %s successfully authenticated", c.Request.RemoteAddr, user)
} 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")
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(200, gin.H{
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
return
})
// All following routes need authentication
r.GET("/:ou/:cn/:class", basicAuth, func(c *gin.Context) {
/* panic: ':ou' in new path '/:ou/:cn/:class' conflicts with existing wildcard ':dn' in existing prefix '/:dn'
r.GET("/:dn", ldapBasicAuth, func(c *gin.Context) {
dn := c.Param("dn")
// 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")
res, err := searchByDn(ldapCon, dn, "ALL")
// If DN does not exist, we'll get err='LDAP Result Code 32 "No Such Object"'
if err != nil {
if strings.Contains(err.Error(), "LDAP Result Code 32") {
c.AbortWithError(http.StatusNotFound, err)
return
} else {
log.Errorf("Error searching %s: %v", dn, err)
c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
sendResponse(c, res, format)
return
})*/
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 {
res, err := searchByCn(ldapCon, ou, cn, class, "ALL")
// If OU does not exist, we'll get err='LDAP Result Code 32 "No Such Object"'
if err != nil {
if strings.Contains(err.Error(), "LDAP Result Code 32") {
c.AbortWithError(http.StatusNotFound, err)
return
} else {
log.Errorf("Error searching %s in %s : %v", cn, ou, err)
c.AbortWithError(500, err)
c.AbortWithError(http.StatusInternalServerError, 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")
log.Printf("Format : %s", format)
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 {
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/: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 := searchByCn(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")
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 {
c.AbortWithError(500, err)
log.Error(err)
c.AbortWithError(http.StatusInternalServerError, nil)
return
}
res, err := searchByCn(ldapCon, ou, cn, class, attr)
if err != nil {
if strings.Contains(err.Error(), "LDAP Result Code 32") {
c.AbortWithError(http.StatusNotFound, err)
return
} else {
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
}
if modified {
res, err := doLdapSearch(myldap, ou, cn, class, attr)
res, err := searchByCn(ldapCon, ou, cn, class, attr)
if err != nil {
log.Errorf("Error searching %s in %s : %v", cn, ou, err)
c.AbortWithError(500, err)
c.AbortWithError(http.StatusInternalServerError, err)
return
}
sendResponse(c, res, format)
} else {
c.String(304, "")
c.String(http.StatusNotModified, "")
}
return
})
/* 2 call methods : Either DN in url, or DN in body using /add :
* * curl -u "admin:admin" -H "Content-Type: application/json" -X POST
* --data '{"objectClass":["person","top"],"cn":"newuser","sn":"New"}' \
* https://localhost:8443/cn=newuser,ou=users,dc=example,dc=org
*
* curl -u "admin:admin" -H "Content-Type: application/json" -X POST
* --data '{"dn":"cn=newuser,ou=users,dc=example,dc=org","objectClass":["person","top"],"cn":"newuser","sn":"New"}' \
* https://localhost:8443/add
*/
r.POST("/:dn", ldapBasicAuth, func(c *gin.Context) {
dn := c.Param("dn")
// Get user authenticated LDAP connection from context
ldapCon, err := getLdapConFromContext(c)
if err != nil {
//log.Error(err)
c.AbortWithError(http.StatusInternalServerError, err)
return
}
// Unmarshall json body to a map
if c.Request.Header.Get("Content-Type") == "application/json" {
var attributes map[string]interface{}
err := c.ShouldBindJSON(&attributes)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
// Get dn in body if called with "http://1.2.3.4/add"
if strings.EqualFold(dn, "add") {
dn = attributes["dn"].(string)
}
if len(dn) == 0 {
c.AbortWithError(http.StatusBadRequest, err)
return
}
err = createEntry(ldapCon, dn, attributes)
if err != nil {
if strings.Contains(err.Error(), "LDAP Result Code 50") {
c.AbortWithStatus(http.StatusUnauthorized)
return
// "Entry Already Exists"
} else if strings.Contains(err.Error(), "LDAP Result Code 68") {
c.JSON(http.StatusCreated, gin.H{"message": "Entry already exists"})
/* This returns 201/Created with Location header, although 303/SeeOther is specified
* c.Render(http.StatusSeeOther, render.Redirect{
Code: 303,
Location: fmt.Sprintf("http://1.2.3.4/%s", dn),
Request: c.Request,
})*/
return
} else {
c.AbortWithError(http.StatusBadRequest, err)
return
}
}
c.JSON(http.StatusCreated, gin.H{"message": "Successfully created"})
}
})
/*
* curl -u "admin:admin" --header "Content-Type: application/json" -X PUT
* --data '{"objectClass":["person","top"],"cn":"newuser","sn":"New","description":"Test account"}' \
* https://localhost:8443/cn=newuser,ou=users,dc=example,dc=org
* or
* curl -u "admin:admin" -H "Content-Type: application/json" -X PUT
* -d '{"dn":"cn=newuser,ou=users,dc=example,dc=org", \
* "objectClass":["person","top"],"cn":"newuser","sn":"New","description":"Test account"}' \
* https://localhost:8443/modify
*/
r.PUT("/:dn", ldapBasicAuth, func(c *gin.Context) {
dn := c.Param("dn")
// Get user authenticated LDAP connection from context
ldapCon, err := getLdapConFromContext(c)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
// Unmarshall json body to a map
if c.Request.Header.Get("Content-Type") == "application/json" {
var attributes map[string]interface{}
err := c.ShouldBindJSON(&attributes)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
// Get dn in body if called with "http://1.2.3.4/modify"
if strings.EqualFold(dn, "modify") {
dn = attributes["dn"].(string)
}
if len(dn) == 0 {
c.AbortWithError(http.StatusBadRequest, err)
return
}
err = updateEntry(ldapCon, dn, attributes)
if err != nil {
if strings.Contains(err.Error(), "LDAP Result Code 50") {
c.AbortWithStatus(http.StatusUnauthorized)
return
} else if strings.Contains(err.Error(), "No modification required") {
c.JSON(http.StatusNoContent, gin.H{"message": "No modification required"})
return
} else {
c.AbortWithError(http.StatusBadRequest, err)
return
}
}
c.JSON(http.StatusCreated, gin.H{"message": "Successfully updated"})
}
})
/* 2 call methods : Either DN in url, or DN in body using /delete :
* curl -i -u "admin:admin" -X DELETE https://localhost:8443/cn=newuser,ou=users,dc=example,dc=org
* or
* curl -i -u "admin:admin" -X DELETE -H "Content-Type: application/json" -d '{"dn":"cn=newuser,ou=users,dc=example,dc=org"}' https://localhost:8443/delete
*
* Each leaf have to be deleted (cannot delete if subordinates)
*/
r.DELETE("/:dn", ldapBasicAuth, func(c *gin.Context) {
dn := c.Param("dn")
// Get user authenticated LDAP connection from context
ldapCon, err := getLdapConFromContext(c)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
// Unmarshall json body to a map
if c.Request.Header.Get("Content-Type") == "application/json" {
var attributes map[string]interface{}
err := c.ShouldBindJSON(&attributes)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
// Get dn in body if called with "http://1.2.3.4/delete"
if strings.EqualFold(dn, "delete") {
dn = attributes["dn"].(string)
}
if len(dn) == 0 {
c.AbortWithError(http.StatusBadRequest, err)
return
}
err = deleteEntry(ldapCon, dn)
if err != nil {
//log.Errorf("Error creating %s: %v", dn, err)
c.AbortWithError(http.StatusBadRequest, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Successfully deleted"})
} else {
err = deleteEntry(ldapCon, dn)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
}
})
}
func main() {
@ -269,6 +565,7 @@ func main() {
var ldapUser string
var ldapPass string
var ldapBaseDN string
var ldapAuthBaseDN string
var tlsPrivKey string
var tlsCert string
var doTls bool
@ -277,9 +574,10 @@ 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.StringVar(&ldapAuthBaseDN, "ldap-auth-base-dn", "", "ldap base DN to find authenticating users")
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)")
@ -294,6 +592,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")
@ -334,8 +636,16 @@ func main() {
log.Fatal("No ldap-base-dn defined!")
}
}
if len(ldapAuthBaseDN) == 0 {
l := viper.GetString("LDAP_AUTH_BASE_DN")
if len(l) > 0 {
ldapAuthBaseDN = l
} else {
log.Fatal("No ldap-auth-base-dn defined!")
}
}
if false == doTls {
doTls = viper.GetBool("SERVE_HTTPS")
doTls = viper.GetBool("HTTPS")
}
if doTls && len(tlsCert) == 0 {
l := viper.GetString("SSL_CERTIFICATE")
@ -361,9 +671,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, AuthBaseDN: ldapAuthBaseDN}
_, err := connectLdap(gRoLdap)
if err != nil {
log.Fatalf("Cannot connect to ldap: %v", err)
}
initRouter(r)
if doTls {
r.RunTLS(listen, tlsCert, tlsPrivKey)