Compare commits

..

5 Commits

16 changed files with 789 additions and 722 deletions

View File

@ -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
View File

@ -1,3 +1,3 @@
**/package-lock.json **/go.sum
**/node_modules
**/config.json **/config.json
dist/*

9
Makefile Normal file
View 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
View 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
View 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
View 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
}

View File

@ -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
View 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
)

View 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

View File

@ -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
View File

@ -0,0 +1,9 @@
package main
import (
app "proxmoxaas-ldap/app"
)
func main() {
app.Run()
}

View File

@ -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

View File

@ -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);
});
});
}
}

View File

@ -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 });
}
});

View File

@ -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);
}
};

View File

@ -1,2 +0,0 @@
#!/bin/sh
node .