diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 86a2b41..0000000 --- a/.eslintrc.json +++ /dev/null @@ -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 - } - ] - } -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 97b35b8..4290158 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -**/package-lock.json -**/node_modules -**/config.json \ No newline at end of file +**/go.sum +**/config.json +dist/* \ No newline at end of file diff --git a/app/app.go b/app/app.go new file mode 100644 index 0000000..7deb629 --- /dev/null +++ b/app/app.go @@ -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)) +} diff --git a/app/ldap.go b/app/ldap.go new file mode 100644 index 0000000..29e17dd --- /dev/null +++ b/app/ldap.go @@ -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, + } +} diff --git a/app/utils.go b/app/utils.go new file mode 100644 index 0000000..1898a9e --- /dev/null +++ b/app/utils.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c3baf90 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/package.json b/package.json deleted file mode 100644 index 1aaf305..0000000 --- a/package.json +++ /dev/null @@ -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 ." - } -} diff --git a/proxmoxaas-ldap.go b/proxmoxaas-ldap.go new file mode 100644 index 0000000..1f3a8e7 --- /dev/null +++ b/proxmoxaas-ldap.go @@ -0,0 +1,9 @@ +package main + +import ( + app "proxmoxaas-ldap/app" +) + +func main() { + app.Run() +} diff --git a/src/ldap.js b/src/ldap.js deleted file mode 100644 index 3f093cd..0000000 --- a/src/ldap.js +++ /dev/null @@ -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); - }); - }); - } -} diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 3baf794..0000000 --- a/src/main.js +++ /dev/null @@ -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 }); - } -}); diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index 208589c..0000000 --- a/src/utils.js +++ /dev/null @@ -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); - } -}; diff --git a/start.sh b/start.sh deleted file mode 100755 index c224329..0000000 --- a/start.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -node . \ No newline at end of file