Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
5a07b47a21 | |||
7b3296d8f4 | |||
c74855d064 | |||
bf9e0afccc | |||
5aee108f65 | |||
cb6a7ffaee | |||
d557521a5c | |||
a02b8b9359 | |||
7879f3df08 | |||
6c73cc8b76 | |||
447bdd1128 |
47
README.md
47
README.md
@ -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,8 +17,9 @@ glapi -config glapi.env
|
||||
|
||||
## Configuration file
|
||||
```
|
||||
LISTEN="0.0.0.0: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"
|
||||
|
||||
# This account search for valid user provided by authenticating client.
|
||||
@ -26,6 +28,9 @@ LDAP_BASE_DN="dc=example,dc=org"
|
||||
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
|
||||
@ -33,8 +38,10 @@ 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
|
||||
[
|
||||
@ -175,7 +182,7 @@ hasSubordinates: FALSE
|
||||
[...]
|
||||
```
|
||||
|
||||
### Output format
|
||||
#### Output format
|
||||
Default output is in json. The following formats are supported:
|
||||
- json (default)
|
||||
- text (ini style)
|
||||
@ -215,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"
|
||||
@ -225,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
|
||||
```
|
||||
|
@ -1,13 +1,17 @@
|
||||
LISTEN="0.0.0.0:8080"
|
||||
LDAP_HOST="ldap://ldap.example.org"
|
||||
LDAP_BASE_DN="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 user provided by authenticating client.
|
||||
# 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
|
||||
|
189
ldap.go
189
ldap.go
@ -19,12 +19,35 @@ 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
|
||||
@ -43,7 +66,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,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") || 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,9 +155,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 +166,130 @@ 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, myldap.AuthBaseDN, true, 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
|
||||
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
|
||||
}
|
||||
|
254
main.go
254
main.go
@ -18,10 +18,11 @@ import (
|
||||
"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.2"
|
||||
gVersion = "0.5.5"
|
||||
|
||||
gRoLdap *MyLdap
|
||||
)
|
||||
@ -94,7 +95,6 @@ func sendResponse(c *gin.Context, res *ldap.SearchResult, format string) {
|
||||
txtRes := marshalResultToText(res, "", true, false)
|
||||
log.Debugf("%v\n", string(txtRes))
|
||||
c.String(http.StatusOK, string(txtRes))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -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{
|
||||
@ -222,6 +219,38 @@ func initRouter(r *gin.Engine) {
|
||||
})
|
||||
|
||||
// All following routes need authentication
|
||||
/* 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")
|
||||
@ -238,13 +267,18 @@ 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 {
|
||||
log.Errorf("Error searching %s in %s : %v", cn, ou, err)
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
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
|
||||
@ -272,7 +306,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,11 +335,16 @@ 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)
|
||||
return
|
||||
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
|
||||
@ -334,7 +373,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 +385,177 @@ func initRouter(r *gin.Engine) {
|
||||
}
|
||||
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() {
|
||||
@ -355,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
|
||||
@ -366,6 +577,7 @@ func main() {
|
||||
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)")
|
||||
@ -424,6 +636,14 @@ 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("HTTPS")
|
||||
}
|
||||
@ -451,7 +671,7 @@ func main() {
|
||||
|
||||
r := gin.Default()
|
||||
|
||||
gRoLdap = &MyLdap{Host: ldapHost, User: ldapUser, Pass: ldapPass, BaseDN: ldapBaseDN}
|
||||
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)
|
||||
|
Reference in New Issue
Block a user