Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
5a07b47a21 | |||
7b3296d8f4 | |||
c74855d064 | |||
bf9e0afccc | |||
5aee108f65 | |||
cb6a7ffaee | |||
d557521a5c | |||
a02b8b9359 | |||
7879f3df08 | |||
6c73cc8b76 | |||
447bdd1128 | |||
4a8edd3ebd | |||
f8a8c1a0c8 | |||
c4e980834d | |||
5e363df9b0 | |||
bdda2de936 |
60
README.md
60
README.md
@ -1,12 +1,13 @@
|
|||||||
# glapi
|
# glapi
|
||||||
|
|
||||||
Go Ldap API is an HTTP API to an LDAP backend.
|
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
|
## Usage
|
||||||
Start glapi with parameters on command line:
|
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 :
|
or point it to a configuration file :
|
||||||
@ -16,16 +17,31 @@ glapi -config glapi.env
|
|||||||
|
|
||||||
## Configuration file
|
## Configuration file
|
||||||
```
|
```
|
||||||
LISTEN="127.0.0.1:8080"
|
LISTEN="0.0.0.0:8443"
|
||||||
LDAP_HOST="ldap://ldap.example.org"
|
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_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
|
## Querying API
|
||||||
|
### Search Entries
|
||||||
Search LDAP entries through the whole subtree, specifying organizationalUnit, commnName, objectClass, attribute to retrieve.
|
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
|
% 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:
|
Default output is in json. The following formats are supported:
|
||||||
- json (default)
|
- json (default)
|
||||||
- text (ini style)
|
- 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 :
|
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"
|
% 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"
|
% curl -u admin:admin "http://127.0.0.1:8080/ou=domains/yo/person/mail?format=textvalue-nodn"
|
||||||
yo@example.org
|
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,5 +1,18 @@
|
|||||||
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"
|
# The base DN exposed to API. Could be buried in LDAP tree so we expose only a subset of directory.
|
||||||
LDAP_USER="cn=ldapuser,dc=example,dc=org"
|
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'
|
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
205
ldap.go
@ -19,31 +19,54 @@ type MyLdap struct {
|
|||||||
User string
|
User string
|
||||||
Pass string
|
Pass string
|
||||||
BaseDN string
|
BaseDN string
|
||||||
|
AuthBaseDN string
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
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
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
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,36 +88,59 @@ 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, 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 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") {
|
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)
|
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)
|
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)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
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
|
||||||
|
}
|
||||||
|
510
main.go
510
main.go
@ -9,17 +9,22 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"flag"
|
"flag"
|
||||||
"time"
|
"time"
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
"net/http"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-ldap/ldap/v3"
|
"github.com/go-ldap/ldap/v3"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
//"github.com/gin-gonic/gin/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gVersion = "0.4"
|
gVersion = "0.5.5"
|
||||||
|
|
||||||
|
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 +59,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,28 +74,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))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,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
|
||||||
@ -141,125 +145,417 @@ 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
|
|
||||||
// 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")
|
c.Writer.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
|
||||||
return
|
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) {
|
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) {
|
/* 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")
|
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 := searchByCn(ldapCon, ou, cn, class, "ALL")
|
||||||
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 OU does not exist, we'll get err='LDAP Result Code 32 "No Such Object"'
|
if err != nil {
|
||||||
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)
|
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)
|
|
||||||
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)
|
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 := 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")
|
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 := 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if modified {
|
if modified {
|
||||||
res, err := doLdapSearch(myldap, 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(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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/* 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() {
|
func main() {
|
||||||
@ -269,14 +565,22 @@ func main() {
|
|||||||
var ldapUser string
|
var ldapUser string
|
||||||
var ldapPass string
|
var ldapPass string
|
||||||
var ldapBaseDN string
|
var ldapBaseDN string
|
||||||
|
var ldapAuthBaseDN 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.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)")
|
||||||
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 +592,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 +636,33 @@ func main() {
|
|||||||
log.Fatal("No ldap-base-dn defined!")
|
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")
|
||||||
|
}
|
||||||
|
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 +671,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, AuthBaseDN: ldapAuthBaseDN}
|
||||||
|
_, 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user