Compare commits
No commits in common. "0659ac30252449272df52a46f370bc91b827213c" and "067e327eb8c731b8803e2c960894cb25e7910d82" have entirely different histories.
0659ac3025
...
067e327eb8
42
.eslintrc.json
Normal file
42
.eslintrc.json
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"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 @@
|
|||||||
**/go.sum
|
**/package-lock.json
|
||||||
|
**/node_modules
|
||||||
**/config.json
|
**/config.json
|
||||||
dist/*
|
|
9
Makefile
9
Makefile
@ -1,9 +0,0 @@
|
|||||||
build: clean
|
|
||||||
go build -ldflags="-s -w" -o dist/ .
|
|
||||||
|
|
||||||
test: clean
|
|
||||||
go run .
|
|
||||||
|
|
||||||
clean:
|
|
||||||
go clean
|
|
||||||
rm -f dist/*
|
|
295
app/app.go
295
app/app.go
@ -1,295 +0,0 @@
|
|||||||
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
368
app/ldap.go
@ -1,368 +0,0 @@
|
|||||||
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
48
app/utils.go
@ -1,48 +0,0 @@
|
|||||||
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": 80,
|
"listenPort": 8082,
|
||||||
"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
45
go.mod
@ -1,45 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
@ -1,11 +0,0 @@
|
|||||||
[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
Normal file
29
package.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"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 ."
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
app "proxmoxaas-ldap/app"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app.Run()
|
|
||||||
}
|
|
11
service/proxmoxaas-ldap.service
Normal file
11
service/proxmoxaas-ldap.service
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[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
Normal file
275
src/ldap.js
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
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
Normal file
347
src/main.js
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
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
Normal file
12
src/utils.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
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