diff --git a/ldap.go b/ldap.go index e68e078..0dfbcc7 100644 --- a/ldap.go +++ b/ldap.go @@ -25,6 +25,28 @@ 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 @@ -43,7 +65,7 @@ func connectLdap(myldap *MyLdap) (*MyLdap, error) { 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,30 +87,40 @@ 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, filter, class, attributes) +} + +func doLdapSearch(myldap *MyLdap, baseDn, 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") || 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) 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 } @@ -120,17 +152,137 @@ func doLdapSearch(myldap *MyLdap, baseDn, cn, class, attributes string) (*ldap.S } 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 { return "", err } 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 { - 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 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 +} diff --git a/main.go b/main.go index fa17f67..0349866 100644 --- a/main.go +++ b/main.go @@ -21,7 +21,7 @@ import ( ) var ( - gVersion = "0.5.2" + gVersion = "0.5.3" gRoLdap *MyLdap ) @@ -111,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 @@ -145,7 +145,6 @@ func checkIfModifiedSince(c *gin.Context, myldap *MyLdap, baseDn, cn, class, att return true, nil } - // Basic Authentication handler with local hardcoded account - do not use func basicAuth(c *gin.Context) { user, password, hasAuth := c.Request.BasicAuth() @@ -203,7 +202,6 @@ func ldapBasicAuth(c *gin.Context) { } } - func getLdapConFromContext(c *gin.Context) (*MyLdap, error) { ldapCon, exist := c.Get("ldapCon") if exist != true { @@ -212,7 +210,6 @@ func getLdapConFromContext(c *gin.Context) (*MyLdap, error) { return ldapCon.(*MyLdap), nil } - func initRouter(r *gin.Engine) { r.GET("/ping", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ @@ -238,7 +235,7 @@ func initRouter(r *gin.Engine) { // json format is the default 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 err != nil { @@ -272,7 +269,7 @@ func initRouter(r *gin.Engine) { } if modified { - res, err := doLdapSearch(ldapCon, ou, cn, class, "ALL") + 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) @@ -301,7 +298,7 @@ func initRouter(r *gin.Engine) { return } - res, err := doLdapSearch(ldapCon, 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(http.StatusInternalServerError, err) @@ -334,7 +331,7 @@ func initRouter(r *gin.Engine) { } if modified { - res, err := doLdapSearch(ldapCon, 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(http.StatusInternalServerError, err) @@ -346,6 +343,113 @@ func initRouter(r *gin.Engine) { } 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() {