Rewrite API in GO #1
@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
"env": {
|
|
||||||
"es2021": true,
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"extends": "standard",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": "latest",
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"no-tabs": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"allowIndentationTabs": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"indent": [
|
|
||||||
"error",
|
|
||||||
"tab"
|
|
||||||
],
|
|
||||||
"linebreak-style": [
|
|
||||||
"error",
|
|
||||||
"unix"
|
|
||||||
],
|
|
||||||
"quotes": [
|
|
||||||
"error",
|
|
||||||
"double"
|
|
||||||
],
|
|
||||||
"semi": [
|
|
||||||
"error",
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"brace-style": [
|
|
||||||
"error",
|
|
||||||
"stroustrup",
|
|
||||||
{
|
|
||||||
"allowSingleLine": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,3 +1,3 @@
|
|||||||
**/package-lock.json
|
**/go.sum
|
||||||
**/node_modules
|
|
||||||
**/config.json
|
**/config.json
|
||||||
|
dist/*
|
9
Makefile
Normal file
9
Makefile
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
build: clean
|
||||||
|
go build -ldflags="-s -w" -o dist/ .
|
||||||
|
|
||||||
|
test: clean
|
||||||
|
go run .
|
||||||
|
|
||||||
|
clean:
|
||||||
|
go clean
|
||||||
|
rm -f dist/*
|
295
app/app.go
Normal file
295
app/app.go
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-contrib/sessions/cookie"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-ldap/ldap/v3"
|
||||||
|
uuid "github.com/nu7hatch/gouuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var LDAPSessions map[string]*LDAPClient
|
||||||
|
|
||||||
|
func Run() {
|
||||||
|
gob.Register(LDAPClient{})
|
||||||
|
|
||||||
|
configPath := flag.String("config", "config.json", "path to config.json file")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
config := GetConfig(*configPath)
|
||||||
|
log.Println("Initialized config from " + *configPath)
|
||||||
|
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
router := gin.Default()
|
||||||
|
store := cookie.NewStore([]byte(config.SessionSecretKey))
|
||||||
|
store.Options(sessions.Options{
|
||||||
|
Path: config.SessionCookie.Path,
|
||||||
|
HttpOnly: config.SessionCookie.HttpOnly,
|
||||||
|
Secure: config.SessionCookie.Secure,
|
||||||
|
MaxAge: config.SessionCookie.MaxAge,
|
||||||
|
})
|
||||||
|
router.Use(sessions.Sessions(config.SessionCookieName, store))
|
||||||
|
|
||||||
|
LDAPSessions = make(map[string]*LDAPClient)
|
||||||
|
|
||||||
|
router.POST("/ticket", func(c *gin.Context) {
|
||||||
|
var body Login
|
||||||
|
if err := c.ShouldBind(&body); err != nil { // bad request from binding
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"auth": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newLDAPClient, err := NewLDAPClient(config)
|
||||||
|
if err != nil { // failed to dial ldap server, considered a server error
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"auth": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = newLDAPClient.BindUser(body.Username, body.Password)
|
||||||
|
if err != nil { // failed to authenticate, return error
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"auth": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// successful binding at this point
|
||||||
|
// create new session
|
||||||
|
session := sessions.Default(c)
|
||||||
|
// create (hopefully) safe uuid to map to ldap session
|
||||||
|
uuid, _ := uuid.NewV4()
|
||||||
|
// set uuid mapping in session
|
||||||
|
session.Set("SessionUUID", uuid.String())
|
||||||
|
// set uuid mapping in LDAPSessions
|
||||||
|
LDAPSessions[uuid.String()] = newLDAPClient
|
||||||
|
// save the session
|
||||||
|
session.Save()
|
||||||
|
// return successful auth
|
||||||
|
c.JSON(http.StatusOK, gin.H{"auth": true})
|
||||||
|
})
|
||||||
|
|
||||||
|
router.DELETE("/ticket", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
SessionUUID := session.Get("SessionUUID")
|
||||||
|
if SessionUUID == nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uuid := SessionUUID.(string)
|
||||||
|
delete(LDAPSessions, uuid)
|
||||||
|
session.Options(sessions.Options{MaxAge: -1}) // set max age to -1 so it is deleted
|
||||||
|
_ = session.Save()
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
})
|
||||||
|
|
||||||
|
router.GET("/users", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
SessionUUID := session.Get("SessionUUID")
|
||||||
|
if SessionUUID == nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uuid := SessionUUID.(string)
|
||||||
|
LDAPSession := LDAPSessions[uuid]
|
||||||
|
if LDAPSession == nil { // does not have registered ldap session associated with cookie session
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status, res := LDAPSession.GetAllUsers()
|
||||||
|
c.JSON(status, res)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.POST("/users/:userid", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
SessionUUID := session.Get("SessionUUID")
|
||||||
|
if SessionUUID == nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uuid := SessionUUID.(string)
|
||||||
|
LDAPSession := LDAPSessions[uuid]
|
||||||
|
if LDAPSession == nil { // does not have registered ldap session associated with cookie session
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var body User
|
||||||
|
if err := c.ShouldBind(&body); err != nil { // bad request from binding
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"auth": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if user already exists
|
||||||
|
status, res := LDAPSession.GetUser(c.Param("userid"))
|
||||||
|
if status != 200 && ldap.IsErrorWithCode(res["error"].(error), ldap.LDAPResultNoSuchObject) { // user does not already exist, create new user
|
||||||
|
status, res = LDAPSession.AddUser(c.Param("userid"), body)
|
||||||
|
c.JSON(status, res)
|
||||||
|
} else { // user already exists, attempt to modify user
|
||||||
|
status, res = LDAPSession.ModUser(c.Param("userid"), body)
|
||||||
|
c.JSON(status, res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.GET("/users/:userid", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
SessionUUID := session.Get("SessionUUID")
|
||||||
|
if SessionUUID == nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uuid := SessionUUID.(string)
|
||||||
|
LDAPSession := LDAPSessions[uuid]
|
||||||
|
if LDAPSession == nil { // does not have registered ldap session associated with cookie session
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status, res := LDAPSession.GetUser(c.Param("userid"))
|
||||||
|
c.JSON(status, res)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.DELETE("/users/:userid", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
SessionUUID := session.Get("SessionUUID")
|
||||||
|
if SessionUUID == nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uuid := SessionUUID.(string)
|
||||||
|
LDAPSession := LDAPSessions[uuid]
|
||||||
|
if LDAPSession == nil { // does not have registered ldap session associated with cookie session
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status, res := LDAPSession.DelUser(c.Param("userid"))
|
||||||
|
c.JSON(status, res)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.GET("/groups", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
SessionUUID := session.Get("SessionUUID")
|
||||||
|
if SessionUUID == nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uuid := SessionUUID.(string)
|
||||||
|
LDAPSession := LDAPSessions[uuid]
|
||||||
|
if LDAPSession == nil { // does not have registered ldap session associated with cookie session
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status, res := LDAPSession.GetAllGroups()
|
||||||
|
c.JSON(status, res)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.GET("/groups/:groupid", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
SessionUUID := session.Get("SessionUUID")
|
||||||
|
if SessionUUID == nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uuid := SessionUUID.(string)
|
||||||
|
LDAPSession := LDAPSessions[uuid]
|
||||||
|
if LDAPSession == nil { // does not have registered ldap session associated with cookie session
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status, res := LDAPSession.GetGroup(c.Param("groupid"))
|
||||||
|
c.JSON(status, res)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.POST("/groups/:groupid", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
SessionUUID := session.Get("SessionUUID")
|
||||||
|
if SessionUUID == nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uuid := SessionUUID.(string)
|
||||||
|
LDAPSession := LDAPSessions[uuid]
|
||||||
|
if LDAPSession == nil { // does not have registered ldap session associated with cookie session
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var body Group
|
||||||
|
if err := c.ShouldBind(&body); err != nil { // bad request from binding
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"auth": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if user already exists
|
||||||
|
status, res := LDAPSession.GetGroup(c.Param("groupid"))
|
||||||
|
if status != 200 && ldap.IsErrorWithCode(res["error"].(error), ldap.LDAPResultNoSuchObject) { // user does not already exist, create new user
|
||||||
|
status, res = LDAPSession.AddGroup(c.Param("groupid"), body)
|
||||||
|
c.JSON(status, res)
|
||||||
|
} else { // user already exists, attempt to modify user
|
||||||
|
status, res = LDAPSession.ModGroup(c.Param("groupid"), body)
|
||||||
|
c.JSON(status, res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.DELETE("/groups/:groupid", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
SessionUUID := session.Get("SessionUUID")
|
||||||
|
if SessionUUID == nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uuid := SessionUUID.(string)
|
||||||
|
LDAPSession := LDAPSessions[uuid]
|
||||||
|
if LDAPSession == nil { // does not have registered ldap session associated with cookie session
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status, res := LDAPSession.DelGroup(c.Param("groupid"))
|
||||||
|
c.JSON(status, res)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.POST("/groups/:groupid/members/:userid", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
SessionUUID := session.Get("SessionUUID")
|
||||||
|
if SessionUUID == nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uuid := SessionUUID.(string)
|
||||||
|
LDAPSession := LDAPSessions[uuid]
|
||||||
|
if LDAPSession == nil { // does not have registered ldap session associated with cookie session
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status, res := LDAPSession.AddUserToGroup(c.Param("userid"), c.Param("groupid"))
|
||||||
|
c.JSON(status, res)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.DELETE("/groups/:groupid/members/:userid", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
SessionUUID := session.Get("SessionUUID")
|
||||||
|
if SessionUUID == nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uuid := SessionUUID.(string)
|
||||||
|
LDAPSession := LDAPSessions[uuid]
|
||||||
|
if LDAPSession == nil { // does not have registered ldap session associated with cookie session
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status, res := LDAPSession.DelUserFromGroup(c.Param("userid"), c.Param("groupid"))
|
||||||
|
c.JSON(status, res)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Run("0.0.0.0:" + strconv.Itoa(config.ListenPort))
|
||||||
|
}
|
368
app/ldap.go
Normal file
368
app/ldap.go
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-ldap/ldap/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LDAPClient struct {
|
||||||
|
client *ldap.Conn
|
||||||
|
basedn string
|
||||||
|
peopledn string
|
||||||
|
groupsdn string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLDAPClient(config Config) (*LDAPClient, error) {
|
||||||
|
LDAPConn, err := ldap.DialURL(config.LdapURL)
|
||||||
|
return &LDAPClient{
|
||||||
|
client: LDAPConn,
|
||||||
|
basedn: config.BaseDN,
|
||||||
|
peopledn: "ou=people," + config.BaseDN,
|
||||||
|
groupsdn: "ou=groups," + config.BaseDN,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LDAPClient) BindUser(username string, password string) error {
|
||||||
|
userdn := fmt.Sprintf("uid=%s,%s", username, l.peopledn)
|
||||||
|
return l.client.Bind(userdn, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LDAPClient) GetAllUsers() (int, gin.H) {
|
||||||
|
searchRequest := ldap.NewSearchRequest(
|
||||||
|
l.peopledn, // The base dn to search
|
||||||
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||||
|
"(&(objectClass=inetOrgPerson))", // The filter to apply
|
||||||
|
[]string{"dn", "cn", "sn", "mail", "uid"}, // A list attributes to retrieve
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
searchResponse, err := l.client.Search(searchRequest) // perform search
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, gin.H{
|
||||||
|
"ok": false,
|
||||||
|
"error": err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = []gin.H{} // create list of results
|
||||||
|
|
||||||
|
for _, entry := range searchResponse.Entries { // for each result,
|
||||||
|
results = append(results, gin.H{
|
||||||
|
"dn": entry.DN,
|
||||||
|
"attributes": gin.H{
|
||||||
|
"cn": entry.GetAttributeValue("cn"),
|
||||||
|
"sn": entry.GetAttributeValue("sn"),
|
||||||
|
"mail": entry.GetAttributeValue("mail"),
|
||||||
|
"uid": entry.GetAttributeValue("uid"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusOK, gin.H{
|
||||||
|
"ok": true,
|
||||||
|
"error": nil,
|
||||||
|
"users": results,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LDAPClient) AddUser(uid string, user User) (int, gin.H) {
|
||||||
|
if user.CN == "" || user.SN == "" || user.UserPassword == "" {
|
||||||
|
return http.StatusBadRequest, gin.H{
|
||||||
|
"ok": false,
|
||||||
|
"error": "Missing one of required fields: cn, sn, userpassword",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addRequest := ldap.NewAddRequest(
|
||||||
|
fmt.Sprintf("uid=%s,%s", uid, l.peopledn), // DN
|
||||||
|
nil, // controls
|
||||||
|
)
|
||||||
|
addRequest.Attribute("sn", []string{user.SN})
|
||||||
|
addRequest.Attribute("cn", []string{user.CN})
|
||||||
|
addRequest.Attribute("userPassword", []string{user.CN})
|
||||||
|
addRequest.Attribute("objectClass", []string{"inetOrgPerson"})
|
||||||
|
|
||||||
|
err := l.client.Add(addRequest)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, gin.H{
|
||||||
|
"ok": false,
|
||||||
|
"error": err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusOK, gin.H{
|
||||||
|
"ok": true,
|
||||||
|
"error": nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LDAPClient) GetUser(uid string) (int, gin.H) {
|
||||||
|
searchRequest := ldap.NewSearchRequest( // setup search for user by uid
|
||||||
|
fmt.Sprintf("uid=%s,%s", uid, l.peopledn), // The base dn to search
|
||||||
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||||
|
"(&(objectClass=inetOrgPerson))", // The filter to apply
|
||||||
|
[]string{"dn", "cn", "sn", "mail", "uid"}, // A list attributes to retrieve
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
searchResponse, err := l.client.Search(searchRequest) // perform search
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, gin.H{
|
||||||
|
"ok": false,
|
||||||
|
"error": err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := searchResponse.Entries[0]
|
||||||
|
result := gin.H{
|
||||||
|
"dn": entry.DN,
|
||||||
|
"attributes": gin.H{
|
||||||
|
"cn": entry.GetAttributeValue("cn"),
|
||||||
|
"sn": entry.GetAttributeValue("sn"),
|
||||||
|
"mail": entry.GetAttributeValue("mail"),
|
||||||
|
"uid": entry.GetAttributeValue("uid"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusOK, gin.H{
|
||||||
|
"ok": true,
|
||||||
|
"error": nil,
|
||||||
|
"user": result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LDAPClient) ModUser(uid string, user User) (int, gin.H) {
|
||||||
|
if user.CN == "" && user.SN == "" && user.UserPassword == "" {
|
||||||
|
return http.StatusBadRequest, gin.H{
|
||||||
|
"ok": false,
|
||||||
|
"error": "Requires one of fields: cn, sn, userpassword",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyRequest := ldap.NewModifyRequest(
|
||||||
|
fmt.Sprintf("uid=%s,%s", uid, l.peopledn),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if user.CN != "" {
|
||||||
|
modifyRequest.Replace("cn", []string{user.CN})
|
||||||
|
}
|
||||||
|
if user.SN != "" {
|
||||||
|
modifyRequest.Replace("sn", []string{user.SN})
|
||||||
|
}
|
||||||
|
if user.UserPassword != "" {
|
||||||
|
modifyRequest.Replace("userPassword", []string{user.UserPassword})
|
||||||
|
}
|
||||||
|
|
||||||
|
err := l.client.Modify(modifyRequest)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, gin.H{
|
||||||
|
"ok": false,
|
||||||
|
"error": err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusOK, gin.H{
|
||||||
|
"ok": true,
|
||||||
|
"error": nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LDAPClient) DelUser(uid string) (int, gin.H) {
|
||||||
|
userDN := fmt.Sprintf("uid=%s,%s", uid, l.peopledn)
|
||||||
|
|
||||||
|
// assumes that olcMemberOfRefint=true updates member attributes of referenced groups
|
||||||
|
|
||||||
|
deleteUserRequest := ldap.NewDelRequest( // setup delete request
|
||||||
|
userDN,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
err := l.client.Del(deleteUserRequest) // delete user
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, gin.H{
|
||||||
|
"ok": false,
|
||||||
|
"error": err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusOK, gin.H{
|
||||||
|
"ok": true,
|
||||||
|
"error": nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LDAPClient) GetAllGroups() (int, gin.H) {
|
||||||
|
searchRequest := ldap.NewSearchRequest(
|
||||||
|
l.groupsdn, // The base dn to search
|
||||||
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||||
|
"(&(objectClass=groupOfNames))", // The filter to apply
|
||||||
|
[]string{"cn", "member"}, // A list attributes to retrieve
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
searchResponse, err := l.client.Search(searchRequest) // perform search
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, gin.H{
|
||||||
|
"ok": false,
|
||||||
|
"error": err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = []gin.H{} // create list of results
|
||||||
|
|
||||||
|
for _, entry := range searchResponse.Entries { // for each result,
|
||||||
|
results = append(results, gin.H{
|
||||||
|
"dn": entry.DN,
|
||||||
|
"attributes": gin.H{
|
||||||
|
"cn": entry.GetAttributeValue("cn"),
|
||||||
|
"member": entry.GetAttributeValues("member"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusOK, gin.H{
|
||||||
|
"ok": true,
|
||||||
|
"error": nil,
|
||||||
|
"groups": results,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LDAPClient) GetGroup(gid string) (int, gin.H) {
|
||||||
|
searchRequest := ldap.NewSearchRequest( // setup search for user by uid
|
||||||
|
fmt.Sprintf("cn=%s,%s", gid, l.groupsdn), // The base dn to search
|
||||||
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||||
|
"(&(objectClass=groupOfNames))", // The filter to apply
|
||||||
|
[]string{"cn", "member"}, // A list attributes to retrieve
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
searchResponse, err := l.client.Search(searchRequest) // perform search
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, gin.H{
|
||||||
|
"ok": false,
|
||||||
|
"error": err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := searchResponse.Entries[0]
|
||||||
|
result := gin.H{
|
||||||
|
"dn": entry.DN,
|
||||||
|
"attributes": gin.H{
|
||||||
|
"cn": entry.GetAttributeValue("cn"),
|
||||||
|
"member": entry.GetAttributeValues("member"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusOK, gin.H{
|
||||||
|
"ok": true,
|
||||||
|
"error": nil,
|
||||||
|
"group": result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LDAPClient) AddGroup(gid string, group Group) (int, gin.H) {
|
||||||
|
addRequest := ldap.NewAddRequest(
|
||||||
|
fmt.Sprintf("cn=%s,%s", gid, l.groupsdn), // DN
|
||||||
|
nil, // controls
|
||||||
|
)
|
||||||
|
addRequest.Attribute("cn", []string{gid})
|
||||||
|
addRequest.Attribute("member", []string{""})
|
||||||
|
addRequest.Attribute("objectClass", []string{"groupOfNames"})
|
||||||
|
|
||||||
|
err := l.client.Add(addRequest)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, gin.H{
|
||||||
|
"ok": false,
|
||||||
|
"error": err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusOK, gin.H{
|
||||||
|
"ok": true,
|
||||||
|
"error": nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LDAPClient) ModGroup(gid string, group Group) (int, gin.H) {
|
||||||
|
return 200, gin.H{
|
||||||
|
"ok": true,
|
||||||
|
"error": nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LDAPClient) DelGroup(gid string) (int, gin.H) {
|
||||||
|
groupDN := fmt.Sprintf("cn=%s,%s", gid, l.groupsdn)
|
||||||
|
|
||||||
|
// assumes that memberOf overlay will automatically update referenced memberOf attributes
|
||||||
|
|
||||||
|
deleteGroupRequest := ldap.NewDelRequest( // setup delete request
|
||||||
|
groupDN,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
err := l.client.Del(deleteGroupRequest) // delete group
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, gin.H{
|
||||||
|
"ok": false,
|
||||||
|
"error": err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusOK, gin.H{
|
||||||
|
"ok": true,
|
||||||
|
"error": nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LDAPClient) AddUserToGroup(uid string, gid string) (int, gin.H) {
|
||||||
|
userDN := fmt.Sprintf("uid=%s,%s", uid, l.peopledn)
|
||||||
|
groupDN := fmt.Sprintf("cn=%s,%s", gid, l.groupsdn)
|
||||||
|
|
||||||
|
modifyRequest := ldap.NewModifyRequest( // modify group member value
|
||||||
|
groupDN,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
modifyRequest.Add("member", []string{userDN}) // add user to group member attribute
|
||||||
|
|
||||||
|
err := l.client.Modify(modifyRequest) // modify group
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, gin.H{
|
||||||
|
"ok": false,
|
||||||
|
"error": err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusOK, gin.H{
|
||||||
|
"ok": true,
|
||||||
|
"error": nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LDAPClient) DelUserFromGroup(uid string, gid string) (int, gin.H) {
|
||||||
|
userDN := fmt.Sprintf("uid=%s,%s", uid, l.peopledn)
|
||||||
|
groupDN := fmt.Sprintf("cn=%s,%s", gid, l.groupsdn)
|
||||||
|
|
||||||
|
modifyRequest := ldap.NewModifyRequest( // modify group member value
|
||||||
|
groupDN,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
modifyRequest.Delete("member", []string{userDN}) // remove user from group member attribute
|
||||||
|
|
||||||
|
err := l.client.Modify(modifyRequest) // modify group
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, gin.H{
|
||||||
|
"ok": false,
|
||||||
|
"error": err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusOK, gin.H{
|
||||||
|
"ok": true,
|
||||||
|
"error": nil,
|
||||||
|
}
|
||||||
|
}
|
48
app/utils.go
Normal file
48
app/utils.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ListenPort int `json:"listenPort"`
|
||||||
|
LdapURL string `json:"ldapURL"`
|
||||||
|
BaseDN string `json:"baseDN"`
|
||||||
|
SessionSecretKey string `json:"sessionSecretKey"`
|
||||||
|
SessionCookieName string `json:"sessionCookieName"`
|
||||||
|
SessionCookie struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
HttpOnly bool `json:"httpOnly"`
|
||||||
|
Secure bool `json:"secure"`
|
||||||
|
MaxAge int `json:"maxAge"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetConfig(configPath string) Config {
|
||||||
|
content, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error when opening config file: ", err)
|
||||||
|
}
|
||||||
|
var config Config
|
||||||
|
err = json.Unmarshal(content, &config)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error during parsing config file: ", err)
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
type Login struct { // login body struct
|
||||||
|
Username string `form:"username" binding:"required"`
|
||||||
|
Password string `form:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct { // add or modify user body struct
|
||||||
|
CN string `form:"cn"`
|
||||||
|
SN string `form:"sn"`
|
||||||
|
UserPassword string `form:"userpassword"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Group struct { // add or modify group body struct
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"listenPort": 8082,
|
"listenPort": 80,
|
||||||
"ldapURL": "ldap://localhost",
|
"ldapURL": "ldap://localhost",
|
||||||
"basedn": "dc=example,dc=com",
|
"basedn": "dc=example,dc=com",
|
||||||
"sessionSecretKey": "super secret key",
|
"sessionSecretKey": "super secret key",
|
45
go.mod
Normal file
45
go.mod
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
module proxmoxaas-ldap
|
||||||
|
|
||||||
|
go 1.22.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-contrib/sessions v1.0.1
|
||||||
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.8
|
||||||
|
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
|
github.com/bytedance/sonic v1.11.8 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.22.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.3 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/gorilla/context v1.1.2 // indirect
|
||||||
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
|
github.com/gorilla/sessions v1.3.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
golang.org/x/crypto v0.24.0 // indirect
|
||||||
|
golang.org/x/net v0.26.0 // indirect
|
||||||
|
golang.org/x/sys v0.21.0 // indirect
|
||||||
|
golang.org/x/text v0.16.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
11
init/proxmoxaas-ldap.service
Normal file
11
init/proxmoxaas-ldap.service
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=proxmoxaas-ldap
|
||||||
|
After=network.target
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=/<path to dir>
|
||||||
|
ExecStart=/<path to dir>/proxmoxaas-ldap
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
Type=simple
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
29
package.json
29
package.json
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "proxmoxaas-ldap",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"description": "LDAP intermediate API for ProxmoxAAS",
|
|
||||||
"main": "src/main.js",
|
|
||||||
"type": "module",
|
|
||||||
"dependencies": {
|
|
||||||
"axios": "^1.5.1",
|
|
||||||
"body-parser": "^1.20.1",
|
|
||||||
"cookie": "^0.5.0",
|
|
||||||
"cookie-parser": "^1.4.6",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"express-session": "^1.17.3",
|
|
||||||
"ldapjs": "^3.0.5",
|
|
||||||
"minimist": "^1.2.8",
|
|
||||||
"morgan": "^1.10.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"eslint": "^8.43.0",
|
|
||||||
"eslint-config-standard": "^17.1.0",
|
|
||||||
"eslint-plugin-import": "^2.27.5",
|
|
||||||
"eslint-plugin-n": "^16.0.1",
|
|
||||||
"eslint-plugin-promise": "^6.1.1"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"lint": "DEBUG=eslint:cli-engine eslint --fix ."
|
|
||||||
}
|
|
||||||
}
|
|
9
proxmoxaas-ldap.go
Normal file
9
proxmoxaas-ldap.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
app "proxmoxaas-ldap/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app.Run()
|
||||||
|
}
|
@ -1,11 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=proxmoxaas-ldap
|
|
||||||
After=network.target
|
|
||||||
[Service]
|
|
||||||
WorkingDirectory=/<path to dir>/ProxmoxAAS-LDAP/
|
|
||||||
ExecStart=/<path to dir>/ProxmoxAAS-LDAP/start.sh
|
|
||||||
Restart=always
|
|
||||||
RestartSec=10
|
|
||||||
Type=simple
|
|
||||||
[Install]
|
|
||||||
WantedBy=default.target
|
|
275
src/ldap.js
275
src/ldap.js
@ -1,275 +0,0 @@
|
|||||||
import ldap from "ldapjs";
|
|
||||||
|
|
||||||
export default class LDAP {
|
|
||||||
#client = null;
|
|
||||||
#basedn = null;
|
|
||||||
#peopledn = null;
|
|
||||||
#groupsdn = null;
|
|
||||||
|
|
||||||
constructor (url, basedn) {
|
|
||||||
const opts = {
|
|
||||||
url
|
|
||||||
};
|
|
||||||
this.#client = new LDAPJS_CLIENT_ASYNC_WRAPPER(opts);
|
|
||||||
this.#basedn = basedn;
|
|
||||||
this.#peopledn = `ou=people,${basedn}`;
|
|
||||||
this.#groupsdn = `ou=groups,${basedn}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async bindUser (uid, password) {
|
|
||||||
return await this.#client.bind(`uid=${uid},${this.#peopledn}`, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAllUsers () {
|
|
||||||
const result = await this.#client.search(this.#peopledn, {
|
|
||||||
scope: "one"
|
|
||||||
});
|
|
||||||
result.users = result.entries;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async addUser (uid, attrs) {
|
|
||||||
const userDN = `uid=${uid},${this.#peopledn}`;
|
|
||||||
if (!attrs.cn || !attrs.sn || !attrs.userPassword) {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
error: {
|
|
||||||
code: 100,
|
|
||||||
name: "UndefinedAttributeValueError",
|
|
||||||
message: "Undefined Attribute Value"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const entry = {
|
|
||||||
objectClass: "inetOrgPerson",
|
|
||||||
cn: attrs.cn,
|
|
||||||
sn: attrs.sn,
|
|
||||||
uid,
|
|
||||||
userPassword: attrs.userPassword
|
|
||||||
};
|
|
||||||
return await this.#client.add(userDN, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUser (uid) {
|
|
||||||
const result = await this.#client.search(`uid=${uid},${this.#peopledn}`, {});
|
|
||||||
result.user = result.entries[0]; // assume there should only be 1 entry
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async modUser (uid, newAttrs) {
|
|
||||||
const logger = new LDAP_MULTIOP_LOGGER(`modify ${uid}`);
|
|
||||||
for (const attr of ["cn", "sn", "userPassword"]) {
|
|
||||||
if (attr in newAttrs && newAttrs[attr]) { // attr should exist and not be undefined or null
|
|
||||||
const change = new ldap.Change({
|
|
||||||
operation: "replace",
|
|
||||||
modification: {
|
|
||||||
type: attr,
|
|
||||||
values: [newAttrs[attr]]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await this.#client.modify(`uid=${uid},${this.#peopledn}`, change, logger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
async delUser (uid) {
|
|
||||||
const logger = new LDAP_MULTIOP_LOGGER(`del ${uid}`);
|
|
||||||
const userDN = `uid=${uid},${this.#peopledn}`;
|
|
||||||
await this.#client.del(userDN, logger);
|
|
||||||
const groups = await this.#client.search(this.#groupsdn, {
|
|
||||||
scope: "one",
|
|
||||||
filter: `(member=uid=${uid},${this.#peopledn})`
|
|
||||||
}, logger);
|
|
||||||
if (!logger.ok) {
|
|
||||||
return logger;
|
|
||||||
}
|
|
||||||
for (const element of groups.entries) {
|
|
||||||
const change = {
|
|
||||||
operation: "delete",
|
|
||||||
modification: {
|
|
||||||
type: "member",
|
|
||||||
values: [`uid=${uid},${this.#peopledn}`]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
await this.#client.modify(element.dn, change, logger);
|
|
||||||
}
|
|
||||||
return logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAllGroups () {
|
|
||||||
const result = await this.#client.search(this.#groupsdn, {
|
|
||||||
scope: "one"
|
|
||||||
});
|
|
||||||
result.groups = result.entries;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async addGroup (gid) {
|
|
||||||
const groupDN = `cn=${gid},${this.#groupsdn}`;
|
|
||||||
const entry = {
|
|
||||||
objectClass: "groupOfNames",
|
|
||||||
member: "",
|
|
||||||
cn: gid
|
|
||||||
};
|
|
||||||
return await this.#client.add(groupDN, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getGroup (gid) {
|
|
||||||
const result = await this.#client.search(`cn=${gid},${this.#groupsdn}`, {});
|
|
||||||
result.group = result.entries[0]; // assume there should only be 1 entry
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async delGroup (gid) {
|
|
||||||
const groupDN = `cn=${gid},${this.#groupsdn}`;
|
|
||||||
return await this.#client.del(groupDN);
|
|
||||||
}
|
|
||||||
|
|
||||||
async addUserToGroup (uid, gid) {
|
|
||||||
// add the user
|
|
||||||
const change = new ldap.Change({
|
|
||||||
operation: "add",
|
|
||||||
modification: {
|
|
||||||
type: "member",
|
|
||||||
values: [`uid=${uid},${this.#peopledn}`]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return await this.#client.modify(`cn=${gid},${this.#groupsdn}`, change);
|
|
||||||
}
|
|
||||||
|
|
||||||
async delUserFromGroup (uid, gid) {
|
|
||||||
const change = new ldap.Change({
|
|
||||||
operation: "delete",
|
|
||||||
modification: {
|
|
||||||
type: "member",
|
|
||||||
values: [`uid=${uid},${this.#peopledn}`]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return await this.#client.modify(`cn=${gid},${this.#groupsdn}`, change);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LDAP_MULTIOP_LOGGER {
|
|
||||||
op = null;
|
|
||||||
ok = true;
|
|
||||||
error = [];
|
|
||||||
subops = [];
|
|
||||||
constructor (op) {
|
|
||||||
this.op = op;
|
|
||||||
}
|
|
||||||
|
|
||||||
push (op) {
|
|
||||||
if (!op.ok) {
|
|
||||||
this.ok = false;
|
|
||||||
this.error.push(op.error);
|
|
||||||
}
|
|
||||||
this.subops.push(op);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LDAPJS_CLIENT_ASYNC_WRAPPER {
|
|
||||||
#client = null;
|
|
||||||
constructor (opts) {
|
|
||||||
this.#client = ldap.createClient(opts);
|
|
||||||
this.#client.on("error", (err) => {
|
|
||||||
console.error(`An error occured:\n${err}`);
|
|
||||||
});
|
|
||||||
this.#client.on("connectError", (err) => {
|
|
||||||
console.error(`Unable to connect to ${opts.url}:\n${err}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#parseError (err) {
|
|
||||||
if (err) {
|
|
||||||
return { code: err.code, name: err.name, message: err.message };
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bind (dn, password, logger = null) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.#client.bind(dn, password, (err) => {
|
|
||||||
const result = { op: `bind ${dn}`, ok: err === null, error: this.#parseError(err) };
|
|
||||||
if (logger) {
|
|
||||||
logger.push(result);
|
|
||||||
}
|
|
||||||
resolve(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
add (dn, entry, logger = null) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.#client.add(dn, entry, (err) => {
|
|
||||||
const result = { op: `add ${dn}`, ok: err === null, error: this.#parseError(err) };
|
|
||||||
if (logger) {
|
|
||||||
logger.push(result);
|
|
||||||
}
|
|
||||||
resolve(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
search (base, options, logger = null) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.#client.search(base, options, (err, res) => {
|
|
||||||
if (err) {
|
|
||||||
return resolve({ op: `search ${base}`, ok: false, error: err });
|
|
||||||
}
|
|
||||||
const result = { op: `search ${base}`, ok: false, error: null, entries: [] };
|
|
||||||
res.on("searchRequest", (searchRequest) => { });
|
|
||||||
res.on("searchEntry", (entry) => {
|
|
||||||
const attributes = {};
|
|
||||||
for (const element of entry.pojo.attributes) {
|
|
||||||
attributes[element.type] = element.values;
|
|
||||||
}
|
|
||||||
result.entries.push({ dn: entry.pojo.objectName, attributes });
|
|
||||||
});
|
|
||||||
res.on("searchReference", (referral) => { });
|
|
||||||
res.on("error", (err) => {
|
|
||||||
result.ok = false;
|
|
||||||
result.error = this.#parseError(err);
|
|
||||||
if (logger) {
|
|
||||||
logger.push(result);
|
|
||||||
}
|
|
||||||
resolve(result);
|
|
||||||
});
|
|
||||||
res.on("end", (res) => {
|
|
||||||
result.ok = true;
|
|
||||||
result.error = null;
|
|
||||||
if (logger) {
|
|
||||||
logger.push(result);
|
|
||||||
}
|
|
||||||
resolve(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
modify (name, changes, logger = null) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.#client.modify(name, changes, (err) => {
|
|
||||||
const result = { op: `modify ${name} ${changes.operation} ${changes.modification.type}`, ok: err === null, error: this.#parseError(err) };
|
|
||||||
if (logger) {
|
|
||||||
logger.push(result);
|
|
||||||
}
|
|
||||||
resolve(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
del (dn, logger = null) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.#client.del(dn, (err) => {
|
|
||||||
const result = { op: `del ${dn}`, ok: err === null, error: this.#parseError(err) };
|
|
||||||
if (logger) {
|
|
||||||
logger.push(result);
|
|
||||||
}
|
|
||||||
resolve(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
347
src/main.js
347
src/main.js
@ -1,347 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import bodyParser from "body-parser";
|
|
||||||
import cookieParser from "cookie-parser";
|
|
||||||
import morgan from "morgan";
|
|
||||||
import session from "express-session";
|
|
||||||
import parseArgs from "minimist";
|
|
||||||
|
|
||||||
import * as utils from "./utils.js";
|
|
||||||
import LDAP from "./ldap.js";
|
|
||||||
|
|
||||||
global.argv = parseArgs(process.argv.slice(2), {
|
|
||||||
default: {
|
|
||||||
package: "package.json",
|
|
||||||
config: "config/config.json"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
global.utils = utils;
|
|
||||||
global.package = global.utils.readJSONFile(global.argv.package);
|
|
||||||
global.config = global.utils.readJSONFile(global.argv.config);
|
|
||||||
|
|
||||||
const LDAPSessions = {};
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
|
||||||
app.use(cookieParser());
|
|
||||||
app.use(morgan("combined"));
|
|
||||||
app.use(session({
|
|
||||||
secret: global.config.sessionSecretKey,
|
|
||||||
name: global.config.sessionCookieName,
|
|
||||||
cookie: global.config.sessionCookie,
|
|
||||||
resave: false,
|
|
||||||
saveUninitialized: true
|
|
||||||
}));
|
|
||||||
|
|
||||||
app.listen(global.config.listenPort, () => {
|
|
||||||
console.log(`proxmoxaas-ldap v${global.package.version} listening on port ${global.config.listenPort}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET - get API version
|
|
||||||
* responses:
|
|
||||||
* - 200: {version: string}
|
|
||||||
*/
|
|
||||||
app.get("/version", (req, res) => {
|
|
||||||
res.status(200).send({ version: global.package.version });
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET - echo request
|
|
||||||
* responses:
|
|
||||||
* - 200: {body: request.body, cookies: request.cookies}
|
|
||||||
*/
|
|
||||||
app.get("/echo", (req, res) => {
|
|
||||||
res.status(200).send({ body: req.body, cookies: req.cookies });
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST - get session ticket by authenticating using user id and password
|
|
||||||
*/
|
|
||||||
app.post("/ticket", async (req, res) => {
|
|
||||||
const params = {
|
|
||||||
uid: req.body.uid,
|
|
||||||
password: req.body.password
|
|
||||||
};
|
|
||||||
const newLDAPSession = new LDAP(global.config.ldapURL, global.config.basedn);
|
|
||||||
const bindResult = await newLDAPSession.bindUser(params.uid, params.password);
|
|
||||||
if (bindResult.ok) {
|
|
||||||
LDAPSessions[req.session.id] = newLDAPSession;
|
|
||||||
res.status(200).send({ auth: true });
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.status(403).send({
|
|
||||||
ok: bindResult.ok,
|
|
||||||
error: bindResult.error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DELETE - invalidate and remove session ticket
|
|
||||||
*/
|
|
||||||
app.delete("/ticket", async (req, res) => {
|
|
||||||
req.session.ldap = null;
|
|
||||||
req.session.destroy();
|
|
||||||
const expire = new Date(0);
|
|
||||||
res.cookie(global.config.sessionCookieName, "", { expires: expire });
|
|
||||||
res.send({ auth: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET - get user attributes for all users
|
|
||||||
*/
|
|
||||||
app.get("/users", async (req, res) => {
|
|
||||||
if (req.session.id in LDAPSessions) {
|
|
||||||
const ldap = LDAPSessions[req.session.id];
|
|
||||||
const result = await ldap.getAllUsers();
|
|
||||||
res.send({
|
|
||||||
ok: result.ok,
|
|
||||||
error: result.error,
|
|
||||||
users: result.users
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.status(403).send({ auth: false });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST - create a new user or modify existing user attributes
|
|
||||||
* request:
|
|
||||||
* - userid: user id
|
|
||||||
* - cn: common name
|
|
||||||
* - sn: surname
|
|
||||||
* - userpassword: user password
|
|
||||||
*/
|
|
||||||
app.post("/users/:userid", async (req, res) => {
|
|
||||||
const params = {
|
|
||||||
userid: req.params.userid,
|
|
||||||
userattrs: {
|
|
||||||
cn: req.body.usercn,
|
|
||||||
sn: req.body.usersn,
|
|
||||||
userPassword: req.body.userpassword
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (req.session.id in LDAPSessions) {
|
|
||||||
const ldap = LDAPSessions[req.session.id];
|
|
||||||
const checkUser = await ldap.getUser(params.userid);
|
|
||||||
if (!checkUser.ok && checkUser.error.code === 32) { // the user does not exist, create new user
|
|
||||||
const result = await ldap.addUser(params.userid, params.userattrs);
|
|
||||||
res.send({
|
|
||||||
ok: result.ok,
|
|
||||||
error: result.error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (checkUser.ok) { // the user does exist, modify the user entries
|
|
||||||
const result = await ldap.modUser(params.userid, params.userattrs);
|
|
||||||
res.send({
|
|
||||||
ok: result.ok,
|
|
||||||
error: result.error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else { // some other error happened
|
|
||||||
res.send({
|
|
||||||
ok: checkUser.ok,
|
|
||||||
error: checkUser.error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.status(403).send({ auth: false });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET - get user attributes
|
|
||||||
* request:
|
|
||||||
* - userid: user id
|
|
||||||
*/
|
|
||||||
app.get("/users/:userid", async (req, res) => {
|
|
||||||
const params = {
|
|
||||||
userid: req.params.userid
|
|
||||||
};
|
|
||||||
if (req.session.id in LDAPSessions) {
|
|
||||||
const ldap = LDAPSessions[req.session.id];
|
|
||||||
const result = await ldap.getUser(params.userid);
|
|
||||||
if (result.ok) {
|
|
||||||
res.send({
|
|
||||||
ok: result.ok,
|
|
||||||
error: result.error,
|
|
||||||
user: result.user
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.send({
|
|
||||||
ok: result.ok,
|
|
||||||
error: result.error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.status(403).send({ auth: false });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DELETE - delete user
|
|
||||||
* request:
|
|
||||||
* - userid: user id
|
|
||||||
*/
|
|
||||||
app.delete("/users/:userid", async (req, res) => {
|
|
||||||
const params = {
|
|
||||||
userid: req.params.userid
|
|
||||||
};
|
|
||||||
if (req.session.id in LDAPSessions) {
|
|
||||||
const ldap = LDAPSessions[req.session.id];
|
|
||||||
const result = await ldap.delUser(params.userid);
|
|
||||||
res.send({
|
|
||||||
ok: result.ok,
|
|
||||||
error: result.error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.status(403).send({ auth: false });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET - get group attributes including members for all groups
|
|
||||||
* request:
|
|
||||||
*/
|
|
||||||
app.get("/groups", async (req, res) => {
|
|
||||||
if (req.session.id in LDAPSessions) {
|
|
||||||
const ldap = LDAPSessions[req.session.id];
|
|
||||||
const result = await ldap.getAllGroups();
|
|
||||||
res.send({
|
|
||||||
ok: result.ok,
|
|
||||||
error: result.error,
|
|
||||||
groups: result.groups
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.status(403).send({ auth: false });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST - create a new group
|
|
||||||
* request:
|
|
||||||
* - groupid: group id
|
|
||||||
*/
|
|
||||||
app.post("/groups/:groupid", async (req, res) => {
|
|
||||||
const params = {
|
|
||||||
groupid: req.params.groupid
|
|
||||||
};
|
|
||||||
if (req.session.id in LDAPSessions) {
|
|
||||||
const ldap = LDAPSessions[req.session.id];
|
|
||||||
const result = await ldap.addGroup(params.groupid);
|
|
||||||
res.send({
|
|
||||||
ok: result.ok,
|
|
||||||
error: result.error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.status(403).send({ auth: false });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET - get group attributes including members
|
|
||||||
* request:
|
|
||||||
* - groupid: group id
|
|
||||||
*/
|
|
||||||
app.get("/groups/:groupid", async (req, res) => {
|
|
||||||
const params = {
|
|
||||||
groupid: req.params.groupid
|
|
||||||
};
|
|
||||||
if (req.session.id in LDAPSessions) {
|
|
||||||
const ldap = LDAPSessions[req.session.id];
|
|
||||||
const result = await ldap.getGroup(params.groupid);
|
|
||||||
if (result.ok) {
|
|
||||||
res.send({
|
|
||||||
ok: result.ok,
|
|
||||||
error: result.error,
|
|
||||||
group: result.group
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.send({
|
|
||||||
ok: result.ok,
|
|
||||||
error: result.error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.status(403).send({ auth: false });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DELETE - delete group
|
|
||||||
* request:
|
|
||||||
* - groupid: group id
|
|
||||||
*/
|
|
||||||
app.delete("/groups/:groupid", async (req, res) => {
|
|
||||||
const params = {
|
|
||||||
groupid: req.params.groupid
|
|
||||||
};
|
|
||||||
if (req.session.id in LDAPSessions) {
|
|
||||||
const ldap = LDAPSessions[req.session.id];
|
|
||||||
const result = await ldap.delGroup(params.groupid);
|
|
||||||
res.send({
|
|
||||||
ok: result.ok,
|
|
||||||
error: result.error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.status(403).send({ auth: false });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST - add a member to the group
|
|
||||||
* request:
|
|
||||||
* - groupid: group id
|
|
||||||
* - userid: user id
|
|
||||||
*/
|
|
||||||
app.post("/groups/:groupid/members/:userid", async (req, res) => {
|
|
||||||
const params = {
|
|
||||||
groupid: req.params.groupid,
|
|
||||||
userid: req.params.userid
|
|
||||||
};
|
|
||||||
if (req.session.id in LDAPSessions) {
|
|
||||||
const ldap = LDAPSessions[req.session.id];
|
|
||||||
const result = await ldap.addUserToGroup(params.userid, params.groupid);
|
|
||||||
res.send({
|
|
||||||
ok: result.ok,
|
|
||||||
error: result.error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.status(403).send({ auth: false });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DELETE - remove a member from the group
|
|
||||||
* - groupid: group id
|
|
||||||
* - userid: user id
|
|
||||||
*/
|
|
||||||
app.delete("/groups/:groupid/members/:userid", async (req, res) => {
|
|
||||||
const params = {
|
|
||||||
groupid: req.params.groupid,
|
|
||||||
userid: req.params.userid
|
|
||||||
};
|
|
||||||
if (req.session.id in LDAPSessions) {
|
|
||||||
const ldap = LDAPSessions[req.session.id];
|
|
||||||
const result = await ldap.delUserFromGroup(params.userid, params.groupid);
|
|
||||||
res.send({
|
|
||||||
ok: result.ok,
|
|
||||||
error: result.error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.status(403).send({ auth: false });
|
|
||||||
}
|
|
||||||
});
|
|
12
src/utils.js
12
src/utils.js
@ -1,12 +0,0 @@
|
|||||||
import { readFileSync } from "fs";
|
|
||||||
import { exit } from "process";
|
|
||||||
|
|
||||||
export function readJSONFile (path) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(readFileSync(path));
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.log(`error: ${path} was not found.`);
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
Loading…
Reference in New Issue
Block a user