Add POST, PUT, DELETE

This commit is contained in:
yo 2022-11-12 20:45:02 +01:00
parent 447bdd1128
commit 6c73cc8b76
2 changed files with 279 additions and 23 deletions

180
ldap.go
View File

@ -25,6 +25,28 @@ var (
mutex sync.Mutex 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) { func connectLdap(myldap *MyLdap) (*MyLdap, error) {
var err error var err error
var conLdap *MyLdap var conLdap *MyLdap
@ -43,7 +65,7 @@ func connectLdap(myldap *MyLdap) (*MyLdap, error) {
return myldap, 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 var err error
if conLdap.Conn == nil { if conLdap.Conn == nil {
conLdap, err = connectLdap(conLdap) conLdap, err = connectLdap(conLdap)
@ -65,30 +87,40 @@ func ldapSearch(conLdap *MyLdap, searchReq *ldap.SearchRequest, attempt int) (*l
return result, err return result, err
} else { } else {
attempt = attempt + 1 attempt = attempt + 1
return ldapSearch(conLdap, searchReq, attempt) return internalLdapSearch(conLdap, searchReq, attempt)
} }
} }
return result, err 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 var filter string
if false == strings.EqualFold(cn, "ALL") {
filter = fmt.Sprintf("(cn=%s)", cn)
} else {
filter = cn
}
return doLdapSearch(myldap, baseDn, filter, class, attributes)
}
func doLdapSearch(myldap *MyLdap, baseDn, filter, class, attributes string) (*ldap.SearchResult, error) {
var fFilter string
var realBaseDn string var realBaseDn string
var realAttributes []string var realAttributes []string
// Build search filter // Build final search filter
if strings.EqualFold(class, "ALL") { if strings.EqualFold(class, "ALL") {
filter = fmt.Sprintf("(objectClass=*)") fFilter = fmt.Sprintf("(objectClass=*)")
} else { } else {
filter = fmt.Sprintf("(objectClass=%s)", class) fFilter = fmt.Sprintf("(objectClass=%s)", class)
} }
if false == strings.EqualFold(cn, "ALL") { if false == strings.EqualFold(filter, "ALL") {
filter = fmt.Sprintf("(&%s(cn=%s))", filter, ldap.EscapeFilter(cn)) 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) // Build absolute search base DN from configuration & provided DN (which is relative)
if strings.EqualFold(baseDn, "ALL") || len(baseDn) == 0 { if strings.EqualFold(baseDn, "ALL") || len(baseDn) == 0 {
@ -109,9 +141,9 @@ func doLdapSearch(myldap *MyLdap, baseDn, cn, class, attributes string) (*ldap.S
log.Debugf("LDAP search attributes to return (all if empty): %v", realAttributes) log.Debugf("LDAP search attributes to return (all if empty): %v", realAttributes)
searchReq := ldap.NewSearchRequest(realBaseDn, ldap.ScopeWholeSubtree, 0, 0, 0, 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 { if err != nil {
return nil, err return nil, err
} }
@ -120,17 +152,137 @@ func doLdapSearch(myldap *MyLdap, baseDn, cn, class, attributes string) (*ldap.S
} }
func findUserFullDN(myldap *MyLdap, username string) (string, error) { func findUserFullDN(myldap *MyLdap, username string) (string, error) {
sr, err := doLdapSearch(myldap, "", username, "ALL", "") filter := fmt.Sprintf("cn=%s", username)
sr, err := doLdapSearch(myldap, "", filter, "ALL", "")
if err != nil { if err != nil {
return "", err return "", err
} }
if len(sr.Entries) == 0 { if len(sr.Entries) == 0 {
return "", fmt.Errorf("User not found with cn=%s", username) return "", fmt.Errorf("User not found with %s", filter)
} else if len(sr.Entries) > 1 { } else if len(sr.Entries) > 1 {
return "", fmt.Errorf("More than one object (%d) found with cn=%s", len(sr.Entries), username) return "", fmt.Errorf("More than one object (%d) found with %s", len(sr.Entries), filter)
} }
result := sr.Entries[0].DN result := sr.Entries[0].DN
return result, nil 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
// 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)
sr, err := doLdapSearch(myldap, bdn, filter, "ALL", "ALL")
if err != nil {
return err
}
if len(sr.Entries) == 0 {
return fmt.Errorf("Object %s not found", filter)
} else if len(sr.Entries) > 1 {
return fmt.Errorf("More than one object (%d) found with %s", len(sr.Entries), filter)
}
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
}

122
main.go
View File

@ -21,7 +21,7 @@ import (
) )
var ( var (
gVersion = "0.5.2" gVersion = "0.5.3"
gRoLdap *MyLdap gRoLdap *MyLdap
) )
@ -111,7 +111,7 @@ func checkIfModifiedSince(c *gin.Context, myldap *MyLdap, baseDn, cn, class, att
log.Debugf("ifModifiedSince: %s", ifModifiedSince) log.Debugf("ifModifiedSince: %s", ifModifiedSince)
res, err := doLdapSearch(myldap, baseDn, cn, class, "modifyTimestamp") res, err := searchByCn(myldap, baseDn, cn, class, "modifyTimestamp")
if err != nil { if err != nil {
log.Errorf("Error searching modifyTimestamp for %s in %s : %v", cn, baseDn, err) log.Errorf("Error searching modifyTimestamp for %s in %s : %v", cn, baseDn, err)
return true, err return true, err
@ -145,7 +145,6 @@ func checkIfModifiedSince(c *gin.Context, myldap *MyLdap, baseDn, cn, class, att
return true, nil return true, nil
} }
// Basic Authentication handler with local hardcoded account - do not use // Basic Authentication handler with local hardcoded account - do not use
func basicAuth(c *gin.Context) { func basicAuth(c *gin.Context) {
user, password, hasAuth := c.Request.BasicAuth() user, password, hasAuth := c.Request.BasicAuth()
@ -203,7 +202,6 @@ func ldapBasicAuth(c *gin.Context) {
} }
} }
func getLdapConFromContext(c *gin.Context) (*MyLdap, error) { func getLdapConFromContext(c *gin.Context) (*MyLdap, error) {
ldapCon, exist := c.Get("ldapCon") ldapCon, exist := c.Get("ldapCon")
if exist != true { if exist != true {
@ -212,7 +210,6 @@ func getLdapConFromContext(c *gin.Context) (*MyLdap, error) {
return ldapCon.(*MyLdap), nil return ldapCon.(*MyLdap), nil
} }
func initRouter(r *gin.Engine) { 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{
@ -238,7 +235,7 @@ func initRouter(r *gin.Engine) {
// json format is the default // json format is the default
format := c.DefaultQuery("format", "json") format := c.DefaultQuery("format", "json")
res, err := doLdapSearch(ldapCon, ou, cn, class, "ALL") 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 OU does not exist, we'll get err='LDAP Result Code 32 "No Such Object"'
if err != nil { if err != nil {
@ -272,7 +269,7 @@ func initRouter(r *gin.Engine) {
} }
if modified { if modified {
res, err := doLdapSearch(ldapCon, ou, cn, class, "ALL") res, err := searchByCn(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)
@ -301,7 +298,7 @@ func initRouter(r *gin.Engine) {
return return
} }
res, err := doLdapSearch(ldapCon, ou, cn, class, attr) res, err := searchByCn(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)
@ -334,7 +331,7 @@ func initRouter(r *gin.Engine) {
} }
if modified { if modified {
res, err := doLdapSearch(ldapCon, ou, cn, class, attr) res, err := searchByCn(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)
@ -346,6 +343,113 @@ func initRouter(r *gin.Engine) {
} }
return return
}) })
/*
* curl -u "admin:admin" --header "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
*/
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
}
err = createEntry(ldapCon, dn, attributes)
if err != nil {
if strings.Contains(err.Error(), "LDAP Result Code 50") {
c.AbortWithStatus(http.StatusUnauthorized)
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
*/
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
}
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"})
}
})
/*
* curl -i -u "admin:admin" -X DELETE https://localhost:8443/cn=newuser,ou=users,dc=example,dc=org
*
* 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
}
err = deleteEntry(ldapCon, dn)
if err != nil {
//log.Errorf("Error creating %s: %v", dn, err)
c.AbortWithError(http.StatusBadRequest, err)
return
}
c.JSON(http.StatusCreated, gin.H{"message": "Successfully deleted"})
})
} }
func main() { func main() {