From 05e0c02fe8b121dff59f7b1af8b7de5420c687bc Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Thu, 27 Jun 2024 02:40:09 +0000 Subject: [PATCH 01/24] rename config.template,json to template.config.json --- configs/{config.template.json => template.config.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename configs/{config.template.json => template.config.json} (100%) diff --git a/configs/config.template.json b/configs/template.config.json similarity index 100% rename from configs/config.template.json rename to configs/template.config.json From d41bca141c94e404afa60ab225592d941acf0815 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Sat, 6 Jul 2024 02:46:10 +0000 Subject: [PATCH 02/24] add version route --- app/app.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/app.go b/app/app.go index 7deb629..2730db2 100644 --- a/app/app.go +++ b/app/app.go @@ -15,6 +15,7 @@ import ( ) var LDAPSessions map[string]*LDAPClient +var APIVersion = "1.0.0" func Run() { gob.Register(LDAPClient{}) @@ -38,6 +39,10 @@ func Run() { LDAPSessions = make(map[string]*LDAPClient) + router.GET("/version", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"version": APIVersion}) + }) + router.POST("/ticket", func(c *gin.Context) { var body Login if err := c.ShouldBind(&body); err != nil { // bad request from binding From 8f8f6bd1e80b2d1472435bb7eaa28414221c619b Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Sat, 6 Jul 2024 03:11:30 +0000 Subject: [PATCH 03/24] add installation instructions to README --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index e69de29..e25ccf7 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,36 @@ +# ProxmoxAAS LDAP - Simple REST API for LDAP + +ProxmoxAAS LDAP provides a simple API for managing users and groups in a simplified LDAP server. Expected LDAP configuration can be initialized using [open-ldap-setup](https://git.tronnet.net/tronnet/open-ldap-setup). + +## Installation + +### Prerequisites + +- Initialized LDAP server with the following configuration + - Structure + - Users: ou=people,... + - objectType: inetOrgPerson + - At least 1 user which is a member of admin group + - Groups: ou=groups,... + - objectType: groupOfNames + - At least 1 admin group + - Permissions: + - Admin group should have write access + - Users should have write access to own attributes (cn, sn, userPassword) + - Enable anonymous binding + - Load MemberOf Policy: + - olcMemberOfDangling: ignore + - olcMemberOfRefInt: TRUE + - olcMemberOfGroupOC: groupOfNames + - olcMemberOfMemberAD: member + - olcMemberOfMemberOfAD: memberOf + - Password Policy and TLS are recommended but not required + +### Installation + +1. Download `proxmoxaas-ldap` binary and `template.config.json` file from [releases](releases) +2. Rename `template.config.json` to `config.json` and modify: + - ldapURL: url to the ldap server ie. `ldap://ldap.domain.net` + - baseDN: base DN ie. `dc=domain,dc=net` + - sessionSecretKey: random value used to randomize cookie values, replace with any sufficiently large random string +3. Run the binary \ No newline at end of file From f11e5ccc3155e664e21b6fd96ea64dc2d6194131 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Thu, 18 Jul 2024 20:22:12 +0000 Subject: [PATCH 04/24] fix default session cookie max age, disable cgo in build --- Makefile | 2 +- configs/template.config.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index ec7cbb6..494cef7 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ build: clean - go build -ldflags="-s -w" -o dist/ . + CGO_ENABLED=0 go build -ldflags="-s -w" -o dist/ . test: clean go run . diff --git a/configs/template.config.json b/configs/template.config.json index 52968a3..d0f3c5c 100644 --- a/configs/template.config.json +++ b/configs/template.config.json @@ -8,6 +8,6 @@ "path": "/", "httpOnly": true, "secure": false, - "maxAge": 7200000 + "maxAge": 7200 } } \ No newline at end of file From bf0596d3855ee631587841cfee737eff7292cc48 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Fri, 26 Jul 2024 01:25:46 +0000 Subject: [PATCH 05/24] add memberOf attribute to users, bump version to 1.0.1 --- app/app.go | 2 +- app/ldap.go | 26 ++++++++++++++------------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/app.go b/app/app.go index 2730db2..01752ac 100644 --- a/app/app.go +++ b/app/app.go @@ -15,7 +15,7 @@ import ( ) var LDAPSessions map[string]*LDAPClient -var APIVersion = "1.0.0" +var APIVersion = "1.0.1" func Run() { gob.Register(LDAPClient{}) diff --git a/app/ldap.go b/app/ldap.go index 29e17dd..5173c41 100644 --- a/app/ldap.go +++ b/app/ldap.go @@ -34,8 +34,8 @@ 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 + "(&(objectClass=inetOrgPerson))", // The filter to apply + []string{"dn", "cn", "sn", "mail", "uid", "memberOf"}, // A list attributes to retrieve nil, ) @@ -53,10 +53,11 @@ func (l LDAPClient) GetAllUsers() (int, gin.H) { 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"), + "cn": entry.GetAttributeValue("cn"), + "sn": entry.GetAttributeValue("sn"), + "mail": entry.GetAttributeValue("mail"), + "uid": entry.GetAttributeValue("uid"), + "memberOf": entry.GetAttributeValues("memberOf"), }, }) } @@ -103,8 +104,8 @@ 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 + "(&(objectClass=inetOrgPerson))", // The filter to apply + []string{"dn", "cn", "sn", "mail", "uid", "memberOf"}, // A list attributes to retrieve nil, ) @@ -120,10 +121,11 @@ func (l LDAPClient) GetUser(uid string) (int, gin.H) { 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"), + "cn": entry.GetAttributeValue("cn"), + "sn": entry.GetAttributeValue("sn"), + "mail": entry.GetAttributeValue("mail"), + "uid": entry.GetAttributeValue("uid"), + "memberOf": entry.GetAttributeValues("memberOf"), }, } From eacc349cac1515398f5147d8b870f2959c22841b Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Sat, 5 Oct 2024 00:08:58 +0000 Subject: [PATCH 06/24] fix critical userPassword bug, improve ldap user/group data handling --- app/app.go | 24 ++++++----- app/ldap.go | 110 +++++++++++++++++++++------------------------------ app/utils.go | 80 ++++++++++++++++++++++++++++++++++++- 3 files changed, 137 insertions(+), 77 deletions(-) diff --git a/app/app.go b/app/app.go index 01752ac..892fe2d 100644 --- a/app/app.go +++ b/app/app.go @@ -15,7 +15,7 @@ import ( ) var LDAPSessions map[string]*LDAPClient -var APIVersion = "1.0.1" +var APIVersion = "1.0.2" func Run() { gob.Register(LDAPClient{}) @@ -122,18 +122,22 @@ func Run() { 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 + var body UserRequired // all user attributes required for new users + if err := c.ShouldBind(&body); err != nil { // attempt to bind user data + c.JSON(http.StatusBadRequest, gin.H{"auth": false, "error": err.Error()}) + return + } status, res = LDAPSession.AddUser(c.Param("userid"), body) c.JSON(status, res) } else { // user already exists, attempt to modify user + var body UserOptional // all user attributes optional for new users + if err := c.ShouldBind(&body); err != nil { // attempt to bind user data + c.JSON(http.StatusBadRequest, gin.H{"auth": false, "error": err.Error()}) + return + } status, res = LDAPSession.ModUser(c.Param("userid"), body) c.JSON(status, res) } @@ -231,12 +235,12 @@ func Run() { return } - // check if user already exists + // check if group 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 + if status != 200 && ldap.IsErrorWithCode(res["error"].(error), ldap.LDAPResultNoSuchObject) { // group does not already exist, create new group status, res = LDAPSession.AddGroup(c.Param("groupid"), body) c.JSON(status, res) - } else { // user already exists, attempt to modify user + } else { // group already exists, attempt to modify group status, res = LDAPSession.ModGroup(c.Param("groupid"), body) c.JSON(status, res) } diff --git a/app/ldap.go b/app/ldap.go index 5173c41..adafb3c 100644 --- a/app/ldap.go +++ b/app/ldap.go @@ -8,6 +8,7 @@ import ( "github.com/go-ldap/ldap/v3" ) +// LDAPClient wrapper struct containing the connection, baseDN, peopleDN, and groupsDN type LDAPClient struct { client *ldap.Conn basedn string @@ -15,6 +16,7 @@ type LDAPClient struct { groupsdn string } +// returns a new LDAPClient from the config func NewLDAPClient(config Config) (*LDAPClient, error) { LDAPConn, err := ldap.DialURL(config.LdapURL) return &LDAPClient{ @@ -25,6 +27,7 @@ func NewLDAPClient(config Config) (*LDAPClient, error) { }, err } +// bind a user using username and password to the LDAPClient func (l LDAPClient) BindUser(username string, password string) error { userdn := fmt.Sprintf("uid=%s,%s", username, l.peopledn) return l.client.Bind(userdn, password) @@ -50,16 +53,8 @@ func (l LDAPClient) GetAllUsers() (int, gin.H) { 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"), - "memberOf": entry.GetAttributeValues("memberOf"), - }, - }) + user := LDAPEntryToLDAPUser(entry) + results = append(results, LDAPUserToGin(user)) } return http.StatusOK, gin.H{ @@ -69,37 +64,6 @@ func (l LDAPClient) GetAllUsers() (int, gin.H) { } } -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 @@ -118,16 +82,9 @@ func (l LDAPClient) GetUser(uid string) (int, gin.H) { } 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"), - "memberOf": entry.GetAttributeValues("memberOf"), - }, - } + + user := LDAPEntryToLDAPUser(entry) + result := LDAPUserToGin(user) return http.StatusOK, gin.H{ "ok": true, @@ -136,7 +93,38 @@ func (l LDAPClient) GetUser(uid string) (int, gin.H) { } } -func (l LDAPClient) ModUser(uid string, user User) (int, gin.H) { +func (l LDAPClient) AddUser(uid string, user UserRequired) (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.UserPassword}) + 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) ModUser(uid string, user UserOptional) (int, gin.H) { if user.CN == "" && user.SN == "" && user.UserPassword == "" { return http.StatusBadRequest, gin.H{ "ok": false, @@ -216,13 +204,8 @@ func (l LDAPClient) GetAllGroups() (int, gin.H) { 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"), - }, - }) + group := LDAPEntryToLDAPGroup(entry) + results = append(results, LDAPGroupToGin(group)) } return http.StatusOK, gin.H{ @@ -250,13 +233,8 @@ func (l LDAPClient) GetGroup(gid string) (int, gin.H) { } entry := searchResponse.Entries[0] - result := gin.H{ - "dn": entry.DN, - "attributes": gin.H{ - "cn": entry.GetAttributeValue("cn"), - "member": entry.GetAttributeValues("member"), - }, - } + group := LDAPEntryToLDAPGroup(entry) + result := LDAPGroupToGin(group) return http.StatusOK, gin.H{ "ok": true, diff --git a/app/utils.go b/app/utils.go index 1898a9e..1bca7fb 100644 --- a/app/utils.go +++ b/app/utils.go @@ -4,6 +4,9 @@ import ( "encoding/json" "log" "os" + + "github.com/gin-gonic/gin" + "github.com/go-ldap/ldap/v3" ) type Config struct { @@ -38,11 +41,86 @@ type Login struct { // login body struct Password string `form:"password" binding:"required"` } -type User struct { // add or modify user body struct +type LDAPUserAttributes struct { + CN string + SN string + Mail string + UID string + MemberOf []string +} + +type LDAPUser struct { + DN string + Attributes LDAPUserAttributes +} + +func LDAPEntryToLDAPUser(entry *ldap.Entry) LDAPUser { + return LDAPUser{ + DN: entry.DN, + Attributes: LDAPUserAttributes{ + CN: entry.GetAttributeValue("cn"), + SN: entry.GetAttributeValue("sn"), + Mail: entry.GetAttributeValue("mail"), + UID: entry.GetAttributeValue("uid"), + MemberOf: entry.GetAttributeValues("memberOf"), + }, + } +} + +func LDAPUserToGin(user LDAPUser) gin.H { + return gin.H{ + "dn": user.DN, + "attributes": gin.H{ + "cn": user.Attributes.CN, + "sn": user.Attributes.SN, + "mail": user.Attributes.Mail, + "uid": user.Attributes.UID, + "memberOf": user.Attributes.MemberOf, + }, + } +} + +type LDAPGroupAttributes struct { + CN string + Member []string +} + +type LDAPGroup struct { + DN string + Attributes LDAPGroupAttributes +} + +func LDAPEntryToLDAPGroup(entry *ldap.Entry) LDAPGroup { + return LDAPGroup{ + DN: entry.DN, + Attributes: LDAPGroupAttributes{ + CN: entry.GetAttributeValue("cn"), + Member: entry.GetAttributeValues("member"), + }, + } +} + +func LDAPGroupToGin(group LDAPGroup) gin.H { + return gin.H{ + "dn": group.DN, + "attributes": gin.H{ + "cn": group.Attributes.CN, + "member": group.Attributes.Member, + }, + } +} + +type UserOptional struct { // add or modify user body struct CN string `form:"cn"` SN string `form:"sn"` UserPassword string `form:"userpassword"` } +type UserRequired struct { // add or modify user body struct + CN string `form:"cn" binding:"required"` + SN string `form:"sn" binding:"required"` + UserPassword string `form:"userpassword" binding:"required"` +} + type Group struct { // add or modify group body struct } From 8cefdb0b0123ee7a2c63ff735d2daec893c3b7cc Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Thu, 10 Oct 2024 20:57:29 +0000 Subject: [PATCH 07/24] update go version and dependencies --- go.mod | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index c3baf90..c0d74a2 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module proxmoxaas-ldap -go 1.22.4 +go 1.23 + +toolchain go1.23.2 require ( github.com/gin-contrib/sessions v1.0.1 @@ -11,35 +13,36 @@ require ( 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/bytedance/sonic v1.12.3 // indirect + github.com/bytedance/sonic/loader v0.2.0 // 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/gabriel-vasile/mimetype v1.4.5 // 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/go-playground/validator/v10 v10.22.1 // 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/gorilla/sessions v1.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/knz/go-libedit v1.10.1 // 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/pelletier/go-toml/v2 v2.2.3 // 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 + golang.org/x/arch v0.11.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) From 95ad75b20d0311d7781d799d483b32911ad9afa9 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Thu, 10 Oct 2024 20:59:01 +0000 Subject: [PATCH 08/24] go mod tidy --- go.mod | 1 - 1 file changed, 1 deletion(-) diff --git a/go.mod b/go.mod index c0d74a2..038db19 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,6 @@ require ( github.com/gorilla/sessions v1.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect - github.com/knz/go-libedit v1.10.1 // 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 From 03177eb4d9fb4ac857793f3a9ab896d12bcbb48e Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Sat, 12 Oct 2024 22:33:34 +0000 Subject: [PATCH 09/24] add mail attribute to user, bump API version to 1.0.3 --- app/app.go | 10 ++++++++-- app/ldap.go | 12 ++++++++---- app/utils.go | 2 ++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/app/app.go b/app/app.go index 892fe2d..b22f0ef 100644 --- a/app/app.go +++ b/app/app.go @@ -15,16 +15,18 @@ import ( ) var LDAPSessions map[string]*LDAPClient -var APIVersion = "1.0.2" +var APIVersion = "1.0.3" func Run() { gob.Register(LDAPClient{}) + log.Printf("Starting ProxmoxAAS-LDAP version %s\n", APIVersion) + configPath := flag.String("config", "config.json", "path to config.json file") flag.Parse() config := GetConfig(*configPath) - log.Println("Initialized config from " + *configPath) + log.Printf("Read in config from %s\n", *configPath) gin.SetMode(gin.ReleaseMode) router := gin.Default() @@ -37,6 +39,8 @@ func Run() { }) router.Use(sessions.Sessions(config.SessionCookieName, store)) + log.Printf("Started API router and cookie store (Name: %s Params: %+v)\n", config.SessionCookieName, config.SessionCookie) + LDAPSessions = make(map[string]*LDAPClient) router.GET("/version", func(c *gin.Context) { @@ -300,5 +304,7 @@ func Run() { c.JSON(status, res) }) + log.Printf("Starting LDAP API on port %s\n", strconv.Itoa(config.ListenPort)) + router.Run("0.0.0.0:" + strconv.Itoa(config.ListenPort)) } diff --git a/app/ldap.go b/app/ldap.go index adafb3c..8c9bec2 100644 --- a/app/ldap.go +++ b/app/ldap.go @@ -94,10 +94,10 @@ func (l LDAPClient) GetUser(uid string) (int, gin.H) { } func (l LDAPClient) AddUser(uid string, user UserRequired) (int, gin.H) { - if user.CN == "" || user.SN == "" || user.UserPassword == "" { + if user.CN == "" || user.SN == "" || user.UserPassword == "" || user.Mail == "" { return http.StatusBadRequest, gin.H{ "ok": false, - "error": "Missing one of required fields: cn, sn, userpassword", + "error": "Missing one of required fields: cn, sn, mail, userpassword", } } @@ -107,6 +107,7 @@ func (l LDAPClient) AddUser(uid string, user UserRequired) (int, gin.H) { ) addRequest.Attribute("sn", []string{user.SN}) addRequest.Attribute("cn", []string{user.CN}) + addRequest.Attribute("mail", []string{user.Mail}) addRequest.Attribute("userPassword", []string{user.UserPassword}) addRequest.Attribute("objectClass", []string{"inetOrgPerson"}) @@ -125,10 +126,10 @@ func (l LDAPClient) AddUser(uid string, user UserRequired) (int, gin.H) { } func (l LDAPClient) ModUser(uid string, user UserOptional) (int, gin.H) { - if user.CN == "" && user.SN == "" && user.UserPassword == "" { + if user.CN == "" && user.SN == "" && user.UserPassword == "" && user.Mail == "" { return http.StatusBadRequest, gin.H{ "ok": false, - "error": "Requires one of fields: cn, sn, userpassword", + "error": "Requires one of fields: cn, sn, mail, userpassword", } } @@ -142,6 +143,9 @@ func (l LDAPClient) ModUser(uid string, user UserOptional) (int, gin.H) { if user.SN != "" { modifyRequest.Replace("sn", []string{user.SN}) } + if user.Mail != "" { + modifyRequest.Replace("mail", []string{user.Mail}) + } if user.UserPassword != "" { modifyRequest.Replace("userPassword", []string{user.UserPassword}) } diff --git a/app/utils.go b/app/utils.go index 1bca7fb..c4e139a 100644 --- a/app/utils.go +++ b/app/utils.go @@ -113,12 +113,14 @@ func LDAPGroupToGin(group LDAPGroup) gin.H { type UserOptional struct { // add or modify user body struct CN string `form:"cn"` SN string `form:"sn"` + Mail string `form:"mail"` UserPassword string `form:"userpassword"` } type UserRequired struct { // add or modify user body struct CN string `form:"cn" binding:"required"` SN string `form:"sn" binding:"required"` + Mail string `form:"mail" binding:"required"` UserPassword string `form:"userpassword" binding:"required"` } From 5d41b605b99a85b22ca5318e5c9b9c48bf2d5706 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Mon, 14 Oct 2024 22:21:10 +0000 Subject: [PATCH 10/24] add better ldap response error handling --- app/app.go | 31 +++++++++++++++++-------------- app/ldap.go | 15 +++++++++++---- app/utils.go | 24 +++++++++++++++++++----- 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/app/app.go b/app/app.go index b22f0ef..b9b0898 100644 --- a/app/app.go +++ b/app/app.go @@ -15,7 +15,7 @@ import ( ) var LDAPSessions map[string]*LDAPClient -var APIVersion = "1.0.3" +var APIVersion = "1.0.4" func Run() { gob.Register(LDAPClient{}) @@ -25,7 +25,10 @@ func Run() { configPath := flag.String("config", "config.json", "path to config.json file") flag.Parse() - config := GetConfig(*configPath) + config, err := GetConfig(*configPath) + if err != nil { + log.Fatal("Error when reading config file: ", err) + } log.Printf("Read in config from %s\n", *configPath) gin.SetMode(gin.ReleaseMode) @@ -109,7 +112,7 @@ func Run() { } status, res := LDAPSession.GetAllUsers() - c.JSON(status, res) + c.JSON(status, HandleResponse(res)) }) router.POST("/users/:userid", func(c *gin.Context) { @@ -135,7 +138,7 @@ func Run() { return } status, res = LDAPSession.AddUser(c.Param("userid"), body) - c.JSON(status, res) + c.JSON(status, HandleResponse(res)) } else { // user already exists, attempt to modify user var body UserOptional // all user attributes optional for new users if err := c.ShouldBind(&body); err != nil { // attempt to bind user data @@ -143,7 +146,7 @@ func Run() { return } status, res = LDAPSession.ModUser(c.Param("userid"), body) - c.JSON(status, res) + c.JSON(status, HandleResponse(res)) } }) @@ -162,7 +165,7 @@ func Run() { } status, res := LDAPSession.GetUser(c.Param("userid")) - c.JSON(status, res) + c.JSON(status, HandleResponse(res)) }) router.DELETE("/users/:userid", func(c *gin.Context) { @@ -180,7 +183,7 @@ func Run() { } status, res := LDAPSession.DelUser(c.Param("userid")) - c.JSON(status, res) + c.JSON(status, HandleResponse(res)) }) router.GET("/groups", func(c *gin.Context) { @@ -198,7 +201,7 @@ func Run() { } status, res := LDAPSession.GetAllGroups() - c.JSON(status, res) + c.JSON(status, HandleResponse(res)) }) router.GET("/groups/:groupid", func(c *gin.Context) { @@ -216,7 +219,7 @@ func Run() { } status, res := LDAPSession.GetGroup(c.Param("groupid")) - c.JSON(status, res) + c.JSON(status, HandleResponse(res)) }) router.POST("/groups/:groupid", func(c *gin.Context) { @@ -243,10 +246,10 @@ func Run() { status, res := LDAPSession.GetGroup(c.Param("groupid")) if status != 200 && ldap.IsErrorWithCode(res["error"].(error), ldap.LDAPResultNoSuchObject) { // group does not already exist, create new group status, res = LDAPSession.AddGroup(c.Param("groupid"), body) - c.JSON(status, res) + c.JSON(status, HandleResponse(res)) } else { // group already exists, attempt to modify group status, res = LDAPSession.ModGroup(c.Param("groupid"), body) - c.JSON(status, res) + c.JSON(status, HandleResponse(res)) } }) @@ -265,7 +268,7 @@ func Run() { } status, res := LDAPSession.DelGroup(c.Param("groupid")) - c.JSON(status, res) + c.JSON(status, HandleResponse(res)) }) router.POST("/groups/:groupid/members/:userid", func(c *gin.Context) { @@ -283,7 +286,7 @@ func Run() { } status, res := LDAPSession.AddUserToGroup(c.Param("userid"), c.Param("groupid")) - c.JSON(status, res) + c.JSON(status, HandleResponse(res)) }) router.DELETE("/groups/:groupid/members/:userid", func(c *gin.Context) { @@ -301,7 +304,7 @@ func Run() { } status, res := LDAPSession.DelUserFromGroup(c.Param("userid"), c.Param("groupid")) - c.JSON(status, res) + c.JSON(status, HandleResponse(res)) }) log.Printf("Starting LDAP API on port %s\n", strconv.Itoa(config.ListenPort)) diff --git a/app/ldap.go b/app/ldap.go index 8c9bec2..35a4604 100644 --- a/app/ldap.go +++ b/app/ldap.go @@ -1,6 +1,7 @@ package app import ( + "errors" "fmt" "net/http" @@ -96,8 +97,11 @@ func (l LDAPClient) GetUser(uid string) (int, gin.H) { func (l LDAPClient) AddUser(uid string, user UserRequired) (int, gin.H) { if user.CN == "" || user.SN == "" || user.UserPassword == "" || user.Mail == "" { return http.StatusBadRequest, gin.H{ - "ok": false, - "error": "Missing one of required fields: cn, sn, mail, userpassword", + "ok": false, + "error": ldap.NewError( + ldap.LDAPResultUnwillingToPerform, + errors.New("missing one of required fields: cn, sn, mail, userpassword"), + ), } } @@ -128,8 +132,11 @@ func (l LDAPClient) AddUser(uid string, user UserRequired) (int, gin.H) { func (l LDAPClient) ModUser(uid string, user UserOptional) (int, gin.H) { if user.CN == "" && user.SN == "" && user.UserPassword == "" && user.Mail == "" { return http.StatusBadRequest, gin.H{ - "ok": false, - "error": "Requires one of fields: cn, sn, mail, userpassword", + "ok": false, + "error": ldap.NewError( + ldap.LDAPResultUnwillingToPerform, + errors.New("requires one of fields: cn, sn, mail, userpassword"), + ), } } diff --git a/app/utils.go b/app/utils.go index c4e139a..341b407 100644 --- a/app/utils.go +++ b/app/utils.go @@ -2,7 +2,6 @@ package app import ( "encoding/json" - "log" "os" "github.com/gin-gonic/gin" @@ -23,17 +22,17 @@ type Config struct { } } -func GetConfig(configPath string) Config { +func GetConfig(configPath string) (Config, error) { content, err := os.ReadFile(configPath) if err != nil { - log.Fatal("Error when opening config file: ", err) + return Config{}, err } var config Config err = json.Unmarshal(content, &config) if err != nil { - log.Fatal("Error during parsing config file: ", err) + return Config{}, err } - return config + return config, nil } type Login struct { // login body struct @@ -126,3 +125,18 @@ type UserRequired struct { // add or modify user body struct type Group struct { // add or modify group body struct } + +func HandleResponse(response gin.H) gin.H { + if response["error"] != nil { + err := response["error"].(error) + LDAPerr := err.(*ldap.Error) + response["error"] = gin.H{ + "code": LDAPerr.ResultCode, + "result": ldap.LDAPResultCodeMap[LDAPerr.ResultCode], + "message": LDAPerr.Err.Error(), + } + return response + } else { + return response + } +} From ca0832a010b8242bc7b456c43703fd158ddbe73c Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Mon, 14 Oct 2024 22:21:44 +0000 Subject: [PATCH 11/24] update go mod --- go.mod | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 038db19..7ac781e 100644 --- a/go.mod +++ b/go.mod @@ -17,19 +17,21 @@ require ( github.com/bytedance/sonic/loader v0.2.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.5 // indirect + github.com/gabriel-vasile/mimetype v1.4.6 // 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.1 // indirect github.com/goccy/go-json v0.10.3 // indirect + github.com/google/go-cmp v0.6.0 // 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.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/knz/go-libedit v1.10.1 // 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 @@ -42,6 +44,6 @@ require ( golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.19.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/protobuf v1.35.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) From 0689ee46fd706e64689df78f005bbd359aefe36d Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Tue, 15 Oct 2024 21:34:34 +0000 Subject: [PATCH 12/24] improve ModGroup to perform NOP --- app/ldap.go | 17 ++++++++++++++++- go.mod | 1 - 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/ldap.go b/app/ldap.go index 35a4604..efe84b1 100644 --- a/app/ldap.go +++ b/app/ldap.go @@ -278,7 +278,22 @@ func (l LDAPClient) AddGroup(gid string, group Group) (int, gin.H) { } func (l LDAPClient) ModGroup(gid string, group Group) (int, gin.H) { - return 200, gin.H{ + modifyRequest := ldap.NewModifyRequest( + fmt.Sprintf("cn=%s,%s", gid, l.groupsdn), + nil, + ) + + modifyRequest.Replace("cn", []string{gid}) + + 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, } diff --git a/go.mod b/go.mod index 7ac781e..5e3866a 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,6 @@ require ( github.com/gorilla/sessions v1.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect - github.com/knz/go-libedit v1.10.1 // 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 From fd84f9a991b7656c034353683d8e78abc661d38d Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Wed, 16 Oct 2024 05:03:58 +0000 Subject: [PATCH 13/24] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e25ccf7..0f6f768 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ ProxmoxAAS LDAP provides a simple API for managing users and groups in a simplif ### Installation -1. Download `proxmoxaas-ldap` binary and `template.config.json` file from [releases](releases) +1. Download `proxmoxaas-ldap` binary and `template.config.json` file from [releases](https://git.tronnet.net/tronnet/ProxmoxAAS-LDAP/releases) 2. Rename `template.config.json` to `config.json` and modify: - ldapURL: url to the ldap server ie. `ldap://ldap.domain.net` - baseDN: base DN ie. `dc=domain,dc=net` From 99242b70a081941abfe35a85e87386dc1c05a9bf Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Fri, 18 Oct 2024 04:38:26 +0000 Subject: [PATCH 14/24] add starttls support, add starttls option to config --- app/app.go | 5 +++-- app/ldap.go | 12 ++++++++++++ app/utils.go | 1 + configs/template.config.json | 1 + 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/app.go b/app/app.go index b9b0898..a729329 100644 --- a/app/app.go +++ b/app/app.go @@ -15,6 +15,7 @@ import ( ) var LDAPSessions map[string]*LDAPClient +var AppVersion = "1.0.5" var APIVersion = "1.0.4" func Run() { @@ -47,7 +48,7 @@ func Run() { LDAPSessions = make(map[string]*LDAPClient) router.GET("/version", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"version": APIVersion}) + c.JSON(http.StatusOK, gin.H{"version": APIVersion, "app-version": AppVersion}) }) router.POST("/ticket", func(c *gin.Context) { @@ -93,7 +94,7 @@ func Run() { uuid := SessionUUID.(string) delete(LDAPSessions, uuid) session.Options(sessions.Options{MaxAge: -1}) // set max age to -1 so it is deleted - _ = session.Save() + session.Save() c.JSON(http.StatusUnauthorized, gin.H{"auth": false}) }) diff --git a/app/ldap.go b/app/ldap.go index efe84b1..dc44770 100644 --- a/app/ldap.go +++ b/app/ldap.go @@ -1,6 +1,7 @@ package app import ( + "crypto/tls" "errors" "fmt" "net/http" @@ -20,6 +21,17 @@ type LDAPClient struct { // returns a new LDAPClient from the config func NewLDAPClient(config Config) (*LDAPClient, error) { LDAPConn, err := ldap.DialURL(config.LdapURL) + if err != nil { + return nil, err + } + + if config.StartTLS { + err = LDAPConn.StartTLS(&tls.Config{InsecureSkipVerify: true}) + if err != nil { + return nil, err + } + } + return &LDAPClient{ client: LDAPConn, basedn: config.BaseDN, diff --git a/app/utils.go b/app/utils.go index 341b407..d1b9e71 100644 --- a/app/utils.go +++ b/app/utils.go @@ -11,6 +11,7 @@ import ( type Config struct { ListenPort int `json:"listenPort"` LdapURL string `json:"ldapURL"` + StartTLS bool `json:"startTLS"` BaseDN string `json:"baseDN"` SessionSecretKey string `json:"sessionSecretKey"` SessionCookieName string `json:"sessionCookieName"` diff --git a/configs/template.config.json b/configs/template.config.json index d0f3c5c..cfda43d 100644 --- a/configs/template.config.json +++ b/configs/template.config.json @@ -1,6 +1,7 @@ { "listenPort": 80, "ldapURL": "ldap://localhost", + "startTLS": true, "basedn": "dc=example,dc=com", "sessionSecretKey": "super secret key", "sessionCookieName": "PAASLDAPAuthTicket", From b8b0504a70e6a9fcc27293a04dc6793b60623bb9 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Sat, 19 Oct 2024 04:16:17 +0000 Subject: [PATCH 15/24] add unit tests for various utility functions, add integration test for LDAPClient, add aiutomatic openldap configuration for testing through make, add make targets for tests improve make targets for build/clean, update README with build and test instructions --- Makefile | 24 +- README.md | 19 +- scripts/Makefile | 18 + scripts/debconf-slapd.conf | 16 + scripts/gencert.conf | 2 + scripts/setup.conf | 10 + test/bad_config.json | 1 + test/integration_test.go | 1237 ++++++++++++++++++++++++++++++++++++ test/test_config.json | 14 + test/test_utils.go | 156 +++++ test/unit_test.go | 126 ++++ 11 files changed, 1617 insertions(+), 6 deletions(-) create mode 100644 scripts/Makefile create mode 100644 scripts/debconf-slapd.conf create mode 100644 scripts/gencert.conf create mode 100644 scripts/setup.conf create mode 100644 test/bad_config.json create mode 100644 test/integration_test.go create mode 100644 test/test_config.json create mode 100644 test/test_utils.go create mode 100644 test/unit_test.go diff --git a/Makefile b/Makefile index 494cef7..ba6e5f1 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,23 @@ -build: clean - CGO_ENABLED=0 go build -ldflags="-s -w" -o dist/ . +.PHONY: build test clean dev-init -test: clean - go run . +build: clean + @echo "======================== Building Binary =======================" + CGO_ENABLED=0 go build -ldflags="-s -w" -v -o dist/ . + +tests: dev-reinit + @echo "======================== Running Tests =========================" + go test -v -cover -coverpkg=./app/ -coverprofile coverage ./test/ + @echo "======================= Coverage Report ========================" + go tool cover -func=coverage + @rm -f coverage clean: + @echo "======================== Cleaning Project ======================" go clean - rm -f dist/* \ No newline at end of file + rm -f dist/* + +dev-init: + @cd scripts; make dev-init + +dev-reinit: + @cd scripts; make dev-reinit \ No newline at end of file diff --git a/README.md b/README.md index 0f6f768..8f7138c 100644 --- a/README.md +++ b/README.md @@ -33,4 +33,21 @@ ProxmoxAAS LDAP provides a simple API for managing users and groups in a simplif - ldapURL: url to the ldap server ie. `ldap://ldap.domain.net` - baseDN: base DN ie. `dc=domain,dc=net` - sessionSecretKey: random value used to randomize cookie values, replace with any sufficiently large random string -3. Run the binary \ No newline at end of file +3. Run the binary + +## Building and Testing from Source + +Building requires the go toolchain. Testing requires the go toolchain, make, and apt. Currently only supports Debian. + +### Building from Source + +1. Clone the repository +2. Run `go get` to get requirements +3. Run `make` to build the binary + +### Testing Source + +1. Clone the repository +2. Run `go get` to get requirements +3. Run `make dev-init` to install test requirements including openldap (slapd), ldap-utils, debconf-utils +4. Run `make tests` to run all tests \ No newline at end of file diff --git a/scripts/Makefile b/scripts/Makefile new file mode 100644 index 0000000..fccee96 --- /dev/null +++ b/scripts/Makefile @@ -0,0 +1,18 @@ +.PHONY: dev-init + +prerequisites: + @echo "=================== Installing Prerequisites ===================" + apt install debconf-utils slapd ldap-utils sudo gettext + git clone https://git.tronnet.net/tronnet/open-ldap-setup + cd open-ldap-setup/; bash gencert.sh < ../gencert.conf; + rm -rf open-ldap-setup/ + +dev-init: prerequisites dev-reinit + +dev-reinit: + @echo "====================== Initializing Slapd ======================" + cat debconf-slapd.conf | debconf-set-selections + DEBIAN_FRONTEND=noninteractive dpkg-reconfigure slapd + git clone https://git.tronnet.net/tronnet/open-ldap-setup + cd open-ldap-setup/; bash setup.sh < ../setup.conf; + rm -rf open-ldap-setup/ \ No newline at end of file diff --git a/scripts/debconf-slapd.conf b/scripts/debconf-slapd.conf new file mode 100644 index 0000000..15eb4f7 --- /dev/null +++ b/scripts/debconf-slapd.conf @@ -0,0 +1,16 @@ +slapd slapd/password1 password admin +slapd slapd/internal/adminpw password admin +slapd slapd/internal/generated_adminpw password admin +slapd slapd/password2 password admin +slapd slapd/unsafe_selfwrite_acl note +slapd slapd/purge_database boolean true +slapd slapd/domain string test.paasldap +slapd slapd/ppolicy_schema_needs_update select abort installation +slapd slapd/invalid_config boolean true +slapd slapd/move_old_database boolean true +slapd slapd/backend select MDB +slapd shared/organization string paasldap +slapd slapd/dump_database_destdir string /var/backups/slapd-VERSION +slapd slapd/no_configuration boolean false +slapd slapd/dump_database select when needed +slapd slapd/password_mismatch note \ No newline at end of file diff --git a/scripts/gencert.conf b/scripts/gencert.conf new file mode 100644 index 0000000..8217529 --- /dev/null +++ b/scripts/gencert.conf @@ -0,0 +1,2 @@ +paasldap +localhost \ No newline at end of file diff --git a/scripts/setup.conf b/scripts/setup.conf new file mode 100644 index 0000000..8f60b1a --- /dev/null +++ b/scripts/setup.conf @@ -0,0 +1,10 @@ +dc=test,dc=paasldap +adminuser +adminuser@test.paasldap +admin +user +admin123 +admin123 +/etc/ssl/certs/ldap-ca-cert.pem +/etc/ldap/ldap-server-cert.pem +/etc/ldap/ldap-server-key.pem \ No newline at end of file diff --git a/test/bad_config.json b/test/bad_config.json new file mode 100644 index 0000000..09cd3ae --- /dev/null +++ b/test/bad_config.json @@ -0,0 +1 @@ +{,,} \ No newline at end of file diff --git a/test/integration_test.go b/test/integration_test.go new file mode 100644 index 0000000..1a2b081 --- /dev/null +++ b/test/integration_test.go @@ -0,0 +1,1237 @@ +package tests + +// Assumes that the LDAP test server follows the PAAS-LDAP requirements which can be set using https://git.tronnet.net/tronnet/open-ldap-setup. +// Alternatively run `make dev-init` followed by `make test`. +// The integration tests ensures that the LDAP client maintains the security and access control of PAAS-LDAP but likely does not address integration with generic LDAP setups. + +import ( + "fmt" + "net/http" + app "proxmoxaas-ldap/app" + "testing" + + "github.com/gin-gonic/gin" + "github.com/go-ldap/ldap/v3" +) + +var AdminUser = User{ + username: "adminuser", + password: "admin123", + userObj: app.LDAPUser{ + DN: fmt.Sprintf("uid=adminuser,%s", PeopleDN), + Attributes: app.LDAPUserAttributes{ + CN: "admin", + SN: "user", + UID: "adminuser", + Mail: "adminuser@test.paasldap", + MemberOf: []string{ + fmt.Sprintf("cn=adminuser,%s", GroupDN), + fmt.Sprintf("cn=admins,%s", GroupDN), + }, + }, + }, +} + +var InvalidUser = User{ + username: RandString(16), + password: RandString(16), + userObj: app.LDAPUser{}, +} + +var SampleUser = User{ + username: "sampleuser", + password: "sample123", + userObj: app.LDAPUser{ + DN: "uid=sampleuser,ou=people,dc=test,dc=paasldap", + Attributes: app.LDAPUserAttributes{ + CN: "sample", + SN: "user", + UID: "sampleuser", + Mail: "sampleuser@test.paasldap", + MemberOf: []string{}, + }, + }, +} + +var UserDNMap = map[string]User{ + AdminUser.userObj.DN: AdminUser, + SampleUser.userObj.DN: SampleUser, + // invalid user not included because it should not be added as a valid user +} + +var AdminGroup = Group{ + groupname: "admins", + groupObj: app.LDAPGroup{ + DN: fmt.Sprintf("cn=admins,%s", GroupDN), + Attributes: app.LDAPGroupAttributes{ + CN: "admins", + Member: []string{ + fmt.Sprintf("uid=adminuser,%s", PeopleDN), + }, + }, + }, +} + +var AdminUserGroup = Group{ + groupname: "adminuser", + groupObj: app.LDAPGroup{ + DN: fmt.Sprintf("cn=adminuser,%s", GroupDN), + Attributes: app.LDAPGroupAttributes{ + CN: "adminuser", + Member: []string{ + fmt.Sprintf("uid=adminuser,%s", PeopleDN), + }, + }, + }, +} + +var SampleUserGroup = Group{ + groupname: "sampleuser", + groupObj: app.LDAPGroup{ + DN: fmt.Sprintf("cn=sampleuser,%s", GroupDN), + Attributes: app.LDAPGroupAttributes{ + CN: "sampleuser", + Member: []string{ + "", + fmt.Sprintf("uid=sampleuser,%s", PeopleDN), + }, + }, + }, +} + +var InvalidGroup = Group{ + groupname: "invalid", + groupObj: app.LDAPGroup{ + DN: fmt.Sprintf("cn=invalid,%s", GroupDN), + Attributes: app.LDAPGroupAttributes{ + CN: "invalid", + Member: []string{}, + }, + }, +} + +var GroupDNMap = map[string]Group{ + AdminGroup.groupObj.DN: AdminGroup, + AdminUserGroup.groupObj.DN: AdminUserGroup, + SampleUserGroup.groupObj.DN: SampleUserGroup, +} + +func TestClientBind(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertEquals(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // test a valid user bind which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // test an invalid user bind which should return invalid credentials + err = client.BindUser(InvalidUser.username, InvalidUser.password) + AssertLDAPError(t, "BindUser(InvalidUser)", err, ldap.LDAPResultInvalidCredentials) +} + +func TestGetAllUsers(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // get all users anonymously which should succeed + status, res := client.GetAllUsers() + AssertStatus(t, "GetAllUsers() -> status", status, http.StatusOK) + users := res["users"].([]gin.H) + AssertEquals(t, "GetAllUsers() -> len(res)", len(users), 1) + for i := 0; i < len(users); i++ { + user := users[i] + userDN := user["dn"].(string) + expectedUserObj := UserDNMap[userDN].userObj + AssertLDAPUserEquals(t, fmt.Sprintf("GetAllUsers() -> res[%d]", i), user, expectedUserObj) + } + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + newUser := app.UserRequired{ + CN: SampleUser.userObj.Attributes.CN, + SN: SampleUser.userObj.Attributes.SN, + Mail: SampleUser.userObj.Attributes.Mail, + UserPassword: SampleUser.password, + } + + // create new sample user, which should succeed + status, _ = client.AddUser(SampleUser.username, newUser) + AssertStatus(t, "AddUser(SampleUser) -> status", status, http.StatusOK) + + // get all users with admin bind which should succeed + status, res = client.GetAllUsers() + AssertStatus(t, "GetAllUsers() -> status", status, http.StatusOK) + users = res["users"].([]gin.H) + AssertEquals(t, "GetAllUsers() -> len(res)", len(users), 2) + for i := 0; i < len(users); i++ { + user := users[i] + userDN := user["dn"].(string) + expectedUserObj := UserDNMap[userDN].userObj + AssertLDAPUserEquals(t, fmt.Sprintf("GetAllUsers() -> res[%d]", i), user, expectedUserObj) + } + + // bind using sample user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // get all users with sample user bind which should succeed + status, res = client.GetAllUsers() + AssertStatus(t, "GetAllUsers() -> status", status, http.StatusOK) + users = res["users"].([]gin.H) + AssertEquals(t, "GetAllUsers() -> len(res)", len(users), 2) + for i := 0; i < len(users); i++ { + user := users[i] + userDN := user["dn"].(string) + expectedUserObj := UserDNMap[userDN].userObj + AssertLDAPUserEquals(t, fmt.Sprintf("GetAllUsers() -> res[%d]", i), user, expectedUserObj) + } + + // rebind as admin user + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // delete the sample user + status, _ = client.DelUser(SampleUser.username) + AssertStatus(t, "DelUser(SampleUser) -> status", status, http.StatusOK) +} + +// This contrived test shows how difficult it should be for GetAllUsers to return an error +func TestGetAllUsers_InvalidBaseDN(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + config.BaseDN = RandDN(16) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // get all users anonymously which should fail because of the incorrect DN + status, res := client.GetAllUsers() + AssertStatus(t, "GetAllUsers() -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "GetAllUsers() -> result", res["error"].(error), ldap.LDAPResultNoSuchObject) +} + +func TestGetUser_SelfUser(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // get the admin user which should return the expected user + status, res := client.GetUser(AdminUser.username) + AssertStatus(t, "GetUser(AdminUser) -> status", status, http.StatusOK) + AssertLDAPUserEquals(t, "GetUser(AdminUser) -> result", res["user"], AdminUser.userObj) +} + +func TestGetUser_OtherUser(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + newUser := app.UserRequired{ + CN: SampleUser.userObj.Attributes.CN, + SN: SampleUser.userObj.Attributes.SN, + Mail: SampleUser.userObj.Attributes.Mail, + UserPassword: SampleUser.password, + } + + // create new sample user, which should succeed + status, _ := client.AddUser(SampleUser.username, newUser) + AssertStatus(t, "AddUser(SampleUser) -> status", status, http.StatusOK) + + // bind using sample user credentials which should succeed + err = client.BindUser(SampleUser.username, SampleUser.password) + AssertLDAPError(t, "BindUser(SampleUser)", err, ldap.LDAPResultSuccess) + + // try reading the admin user, which should return the expected admin user + status, res := client.GetUser(AdminUser.username) + AssertStatus(t, "GetUser(AdminUser) -> status", status, http.StatusOK) + AssertLDAPUserEquals(t, "GetUser(AdminUser) -> result", res["user"], AdminUser.userObj) + + // rebind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // delete the sample user + status, _ = client.DelUser(SampleUser.username) + AssertStatus(t, "DelUser(SampleUser) -> status", status, http.StatusOK) +} + +func TestGetUser_NoSuchUser(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // get the invalid user which should return NoSuchObject error + status, res := client.GetUser(InvalidUser.username) + AssertStatus(t, "GetUser(InvalidUser) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "GetUser(InvalidUser) -> result", res["error"].(error), ldap.LDAPResultNoSuchObject) +} + +func TestModUser_SelfUser(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + modification := app.UserOptional{ + CN: "testnewcn", + SN: "testnewsn", + Mail: "testnewmail@test.paasldap", + UserPassword: "test345", + } + + ModifiedUser := AdminUser + ModifiedUser.userObj.Attributes.CN = modification.CN + ModifiedUser.userObj.Attributes.SN = modification.SN + ModifiedUser.userObj.Attributes.Mail = modification.Mail + ModifiedUser.password = modification.UserPassword + + // try modification, which should succeed + status, _ := client.ModUser(AdminUser.username, modification) + AssertStatus(t, "ModUser(AdminUser -> ModifiedUser)", status, http.StatusOK) + + // try reading the update, which should return the expected updated user + status, res := client.GetUser(ModifiedUser.username) + AssertStatus(t, "GetUser(ModifiedUser) -> status", status, http.StatusOK) + AssertLDAPUserEquals(t, "GetUser(ModifiedUser) -> result", res["user"], ModifiedUser.userObj) + + // try binding with the original password, which should fail with invalid credentials + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultInvalidCredentials) + + // try binding with the updated password, which should succeed + err = client.BindUser(ModifiedUser.username, ModifiedUser.password) + AssertLDAPError(t, "BindUser(ModifiedUser)", err, ldap.LDAPResultSuccess) + + modification = app.UserOptional{ + CN: AdminUser.userObj.Attributes.CN, + SN: AdminUser.userObj.Attributes.SN, + Mail: AdminUser.userObj.Attributes.Mail, + UserPassword: AdminUser.password, + } + + // revert previous mod, which should not have errors + status, _ = client.ModUser(ModifiedUser.username, modification) + AssertStatus(t, "ModUser(ModifiedUser -> AdminUser)", status, http.StatusOK) + + // try reading the revert, which should return the expected original user + status, res = client.GetUser(AdminUser.username) + AssertStatus(t, "GetUser(AdminUser) -> status", status, http.StatusOK) + AssertLDAPUserEquals(t, "GetUser(AdminUser) -> result", res["user"], AdminUser.userObj) + + // try binding with the original password, which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // try binding with the updated password, which should fail with invalid credentials + err = client.BindUser(ModifiedUser.username, ModifiedUser.password) + AssertLDAPError(t, "BindUser(ModifiedUser)", err, ldap.LDAPResultInvalidCredentials) +} + +func TestModUser_OtherUser(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + newUser := app.UserRequired{ + CN: SampleUser.userObj.Attributes.CN, + SN: SampleUser.userObj.Attributes.SN, + Mail: SampleUser.userObj.Attributes.Mail, + UserPassword: SampleUser.password, + } + + // create new sample user, which should succeed + status, _ := client.AddUser(SampleUser.username, newUser) + AssertStatus(t, "AddUser(SampleUser) -> status", status, http.StatusOK) + + newPassword := RandString(16) + + modification := app.UserOptional{ + UserPassword: newPassword, + } + + // try password modification, which should succeed + status, _ = client.ModUser(SampleUser.username, modification) + AssertStatus(t, "ModUser(SampleUser -> ModifiedUser) -> status", status, http.StatusOK) + + // try binding with the original password, which should fail with invalid credentials + err = client.BindUser(SampleUser.username, SampleUser.password) + AssertLDAPError(t, "BindUser(SampleUser)", err, ldap.LDAPResultInvalidCredentials) + + // try binding with the updated password, which should succeed + err = client.BindUser(SampleUser.username, newPassword) + AssertLDAPError(t, "BindUser(ModifiedUser)", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + modification = app.UserOptional{ + CN: RandString(16), + } + + // try cn modification, which should fail + status, res := client.ModUser(SampleUser.username, modification) + AssertStatus(t, "ModUser(SampleUser -> ModifiedUser) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "BindUser(ModifiedUser)", res["error"].(error), ldap.LDAPResultInsufficientAccessRights) + + // delete the sample user + status, _ = client.DelUser(SampleUser.username) + AssertStatus(t, "DelUser(SampleUser) -> status", status, http.StatusOK) +} + +func TestModUser_NoSuchUser(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + modification := app.UserOptional{ + CN: "invalid", + } + + // try modification, which should fail with NoSuchObject + status, res := client.ModUser(InvalidUser.username, modification) + AssertStatus(t, "ModUser(InvalidUser -> ModifiedUser) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "ModUser(InvalidUser -> ModifiedUser) -> result", res["error"].(error), ldap.LDAPResultNoSuchObject) +} + +func TestModUser_InsufficientPermission(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + newUser := app.UserRequired{ + CN: SampleUser.userObj.Attributes.CN, + SN: SampleUser.userObj.Attributes.SN, + Mail: SampleUser.userObj.Attributes.Mail, + UserPassword: SampleUser.password, + } + + // create new sample user, which should succeed + status, _ := client.AddUser(SampleUser.username, newUser) + AssertStatus(t, "AddUser(SampleUser) -> status", status, http.StatusOK) + + // bind as the new sample user + err = client.BindUser(SampleUser.username, SampleUser.password) + AssertLDAPError(t, "BindUser(SampleUser)", err, ldap.LDAPResultSuccess) + + modification := app.UserOptional{ + CN: "invalid", + } + + // try modification, which should fail with InsufficientAccessRights + status, res := client.ModUser(AdminUser.username, modification) + AssertStatus(t, "ModUser(AdminUser -> ModifiedUser) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "ModUser(AdminUser -> ModifiedUser) -> result", res["error"].(error), ldap.LDAPResultInsufficientAccessRights) + + // rebind as admin user + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // delete the sample user + status, _ = client.DelUser(SampleUser.username) + AssertStatus(t, "DelUser(SampleUser) -> status", status, http.StatusOK) +} + +func TestModUser_MissingRequiredField(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + modification := app.UserOptional{} + + // try modification, which should fail with mising one of cn, sn, mail, or userpassword + status, res := client.ModUser(AdminUser.username, modification) + AssertStatus(t, "ModUser(AdminUser -> ModifiedUser) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "ModUser(AdminUser -> ModifiedUser) -> result", res["error"].(error), ldap.LDAPResultUnwillingToPerform) +} + +func TestModUser_NoAuth(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + newUser := app.UserOptional{ + CN: SampleUser.userObj.Attributes.CN, + SN: SampleUser.userObj.Attributes.SN, + Mail: SampleUser.userObj.Attributes.Mail, + UserPassword: SampleUser.password, + } + + // test mod admin user as anonymous which should fail with AuthenticationRequired + status, res := client.ModUser(AdminUser.username, newUser) + AssertStatus(t, "ModUser(AdminUser -> SampleUser) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "ModUser(AdminUser -> SampleUser) -> result", res["error"].(error), ldap.LDAPResultStrongAuthRequired) +} + +func TestAddGetDelUser(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + newUser := app.UserRequired{ + CN: SampleUser.userObj.Attributes.CN, + SN: SampleUser.userObj.Attributes.SN, + Mail: SampleUser.userObj.Attributes.Mail, + UserPassword: SampleUser.password, + } + + // create new sample user, which should succeed + status, _ := client.AddUser(SampleUser.username, newUser) + AssertStatus(t, "AddUser(SampleUser) -> status", status, http.StatusOK) + + // try reading the new user, which should return the expected sample user + status, res := client.GetUser(SampleUser.username) + AssertStatus(t, "GetUser(SampleUser) -> status", status, http.StatusOK) + AssertLDAPUserEquals(t, "GetUser(SampleUser) -> result", res["user"], SampleUser.userObj) + + // delete the sample user + status, _ = client.DelUser(SampleUser.username) + AssertStatus(t, "DelUser(SampleUser) -> status", status, http.StatusOK) + + // try reading the new user, which should return a an error since it has been deleted + status, res = client.GetUser(SampleUser.username) + AssertStatus(t, "GetUser(SampleUser) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "GetUser(SampleUser) -> result", res["error"].(error), ldap.LDAPResultNoSuchObject) +} + +func TestAddUser_DuplicateUser(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + newUser := app.UserRequired{ + CN: SampleUser.userObj.Attributes.CN, + SN: SampleUser.userObj.Attributes.SN, + Mail: SampleUser.userObj.Attributes.Mail, + UserPassword: SampleUser.password, + } + + // create new sample user, which should succeed + status, _ := client.AddUser(SampleUser.username, newUser) + AssertStatus(t, "AddUser(SampleUser) -> status", status, http.StatusOK) + + // try to create new sample user again, which should fail with object already exists + status, res := client.AddUser(SampleUser.username, newUser) + AssertStatus(t, "AddUser(SampleUser) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "AddUser(SampleUser) -> result", res["error"].(error), ldap.LDAPResultEntryAlreadyExists) + + // delete the sample user + status, _ = client.DelUser(SampleUser.username) + AssertStatus(t, "DelUser(SampleUser) -> status", status, http.StatusOK) +} + +func TestAddUser_InsufficientPermission(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + newUser := app.UserRequired{ + CN: SampleUser.userObj.Attributes.CN, + SN: SampleUser.userObj.Attributes.SN, + Mail: SampleUser.userObj.Attributes.Mail, + UserPassword: SampleUser.password, + } + + // create new sample user, which should succeed + status, _ := client.AddUser(SampleUser.username, newUser) + AssertStatus(t, "AddUser(SampleUser) -> status", status, http.StatusOK) + + // bind as the sample user + err = client.BindUser(SampleUser.username, SampleUser.password) + AssertLDAPError(t, "BindUser(SampleUser)", err, ldap.LDAPResultSuccess) + + // try to create a new user, which should fail with insufficient permission + status, res := client.AddUser(InvalidUser.username, newUser) + AssertStatus(t, "AddUser(InvalidUser) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "AddUser(InvalidUser) -> result", res["error"].(error), ldap.LDAPResultInsufficientAccessRights) + + // rebind as admin user + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // delete the sample user + status, _ = client.DelUser(SampleUser.username) + AssertStatus(t, "DelUser(SampleUser) -> status", status, http.StatusOK) +} + +func TestAddUser_MissingRequiredField(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + newUser := app.UserRequired{} + + // try add invalid user, which should fail with mising all of cn, sn, mail, or userpassword + status, res := client.AddUser(InvalidUser.username, newUser) + AssertStatus(t, "AddUser(InvalidUser) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "AddUser(InvalidUser) -> result", res["error"].(error), ldap.LDAPResultUnwillingToPerform) +} + +func TestAddUser_NoAuth(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + newUser := app.UserRequired{ + CN: SampleUser.userObj.Attributes.CN, + SN: SampleUser.userObj.Attributes.SN, + Mail: SampleUser.userObj.Attributes.Mail, + UserPassword: SampleUser.password, + } + + // test add admin user as anonymous which should fail with AuthenticationRequired + status, res := client.AddUser(SampleUser.username, newUser) + AssertStatus(t, "AddUser(SampleUser) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "AddUser(SampleUser) -> result", res["error"].(error), ldap.LDAPResultStrongAuthRequired) +} + +func TestDelUser_NoSuchUser(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // try delete invalid user, which should fail with NoSuchObject + status, res := client.DelUser(InvalidUser.username) + AssertStatus(t, "DelUser(InvalidUser) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "DelUser(InvalidUser) -> result", res["error"].(error), ldap.LDAPResultNoSuchObject) +} + +func TestDelUser_InsufficientPermission(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + newUser := app.UserRequired{ + CN: SampleUser.userObj.Attributes.CN, + SN: SampleUser.userObj.Attributes.SN, + Mail: SampleUser.userObj.Attributes.Mail, + UserPassword: SampleUser.password, + } + + // create new sample user, which should succeed + status, _ := client.AddUser(SampleUser.username, newUser) + AssertStatus(t, "AddUser(SampleUser) -> status", status, http.StatusOK) + + // bind as the sample user + err = client.BindUser(SampleUser.username, SampleUser.password) + AssertLDAPError(t, "BindUser(SampleUser)", err, ldap.LDAPResultSuccess) + + // try delete admin user, which should fail with InsufficientAccessRights + status, res := client.DelUser(AdminUser.username) + AssertStatus(t, "DelUser(AdminUser) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "DelUser(AdminUser) -> result", res["error"].(error), ldap.LDAPResultInsufficientAccessRights) + + // rebind as admin user + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // delete the sample user + status, _ = client.DelUser(SampleUser.username) + AssertStatus(t, "DelUser(SampleUser) -> status", status, http.StatusOK) +} + +func TestDelUser_NoAuth(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // test delete admin user as anonymous which should fail with AuthenticationRequired + status, res := client.DelUser(AdminUser.username) + AssertStatus(t, "DelUser(AdminUser) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "DelUser(AdminUser) -> result", res["error"].(error), ldap.LDAPResultStrongAuthRequired) +} + +func TestGetAllGroups(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // get all groups anonymously which should succeed + status, res := client.GetAllGroups() + AssertStatus(t, "GetAllGroups() -> status", status, http.StatusOK) + groups := res["groups"].([]gin.H) + AssertEquals(t, "GetAllGroups() -> len(res)", len(groups), 2) + for i := 0; i < len(groups); i++ { + group := groups[i] + groupDN := group["dn"].(string) + expectedGroupObj := GroupDNMap[groupDN].groupObj + AssertLDAPGroupEquals(t, fmt.Sprintf("GetAllGroups() -> res[%d]", i), group, expectedGroupObj) + } + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // get all groups as admin user which should succeed + status, res = client.GetAllGroups() + AssertStatus(t, "GetAllGroups() -> status", status, http.StatusOK) + groups = res["groups"].([]gin.H) + AssertEquals(t, "GetAllGroups() -> len(res)", len(groups), 2) + for i := 0; i < len(groups); i++ { + group := groups[i] + groupDN := group["dn"].(string) + expectedGroupObj := GroupDNMap[groupDN].groupObj + AssertLDAPGroupEquals(t, fmt.Sprintf("GetAllGroups() -> res[%d]", i), group, expectedGroupObj) + } +} + +// This contrived test shows how difficult it should be for GetAllGroups to return an error +func TestGetAllGroups_InvalidBaseDN(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + config.BaseDN = RandDN(16) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // get all groups anonymously which should fail because of the incorrect DN + status, res := client.GetAllGroups() + AssertStatus(t, "GetAllGroups() -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "GetAllGroups() -> result", res["error"].(error), ldap.LDAPResultNoSuchObject) +} + +func TestGetGroup(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // test get admin group anonymously which should succeed + status, res := client.GetGroup(AdminGroup.groupname) + AssertStatus(t, "GetGroup(AdminGroup) -> status", status, http.StatusOK) + AssertLDAPGroupEquals(t, "GetAllGroups(AdminGroup) -> result", res["group"], AdminGroup.groupObj) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // test get admin group as admin user which should succeed + status, res = client.GetGroup(AdminGroup.groupname) + AssertStatus(t, "GetGroup(AdminGroup) -> status", status, http.StatusOK) + AssertLDAPGroupEquals(t, "GetGroup(AdminGroup) -> result", res["group"], AdminGroup.groupObj) +} + +func TestGetGroup_NoSuchGroup(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // test get invalid group anonymously which should fail with NoSuchObject + status, res := client.GetGroup(InvalidGroup.groupname) + AssertStatus(t, "GetGroup(InvalidGroup) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "GetGroup(InvalidGroup) -> result", res["error"].(error), ldap.LDAPResultNoSuchObject) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // test get invalid group as admin user which should fail with NoSuchObject + status, res = client.GetGroup(InvalidGroup.groupname) + AssertStatus(t, "GetGroup(InvalidGroup) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "GetGroup(InvalidGroup) -> result", res["error"].(error), ldap.LDAPResultNoSuchObject) +} + +// ModGroup does nothing since LDAP GroupOfNames does not store any attributes except CN and Members. +// CN should not be changed and Members should be added or removed using the appropriate functions. +// TODO update this when the function actually produces proper results +func TestModGroup(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // test mod admin group as admin which should succeed + status, _ := client.ModGroup(AdminGroup.groupname, app.Group{}) + AssertStatus(t, "ModGroup(AdminGroup -> AdminGroup) -> status", status, http.StatusOK) + + // test get admin group as admin user which should return the same admin group since no operation has been done + status, res := client.GetGroup(AdminGroup.groupname) + AssertStatus(t, "GetGroup(AdminGroup) -> status", status, http.StatusOK) + AssertLDAPGroupEquals(t, "GetGroup(AdminGroup) -> result", res["group"], AdminGroup.groupObj) +} + +func TestModGroup_NoSuchGroup(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // test mod invalid group as sample user which should fail with InsufficientPermission + status, res := client.ModGroup(InvalidGroup.groupname, app.Group{}) + AssertStatus(t, "ModGroup(InvalidGroup -> InvalidGroup) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "ModGroup(InvalidGroup -> InvalidGroup) -> result", res["error"].(error), ldap.LDAPResultNoSuchObject) +} + +func TestModGroup_InsufficientPermission(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + newUser := app.UserRequired{ + CN: SampleUser.userObj.Attributes.CN, + SN: SampleUser.userObj.Attributes.SN, + Mail: SampleUser.userObj.Attributes.Mail, + UserPassword: SampleUser.password, + } + + // create new sample user, which should succeed + status, _ := client.AddUser(SampleUser.username, newUser) + AssertStatus(t, "AddUser(SampleUser) -> status", status, http.StatusOK) + + // bind as the sample user + err = client.BindUser(SampleUser.username, SampleUser.password) + AssertLDAPError(t, "BindUser(SampleUser)", err, ldap.LDAPResultSuccess) + + // test mod admin group as sample user which should fail with InsufficientPermission + status, res := client.ModGroup(AdminGroup.groupname, app.Group{}) + AssertStatus(t, "ModGroup(AdminGroup -> AdminGroup) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "ModGroup(AdminGroup -> AdminGroup) -> result", res["error"].(error), ldap.LDAPResultInsufficientAccessRights) + + // rebind as admin user + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // delete the sample user + status, _ = client.DelUser(SampleUser.username) + AssertStatus(t, "DelUser(SampleUser) -> status", status, http.StatusOK) +} + +func TestModGroup_NoAuth(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // test mod admin group as anonymous which should fail with AuthenticationRequired + status, res := client.ModGroup(AdminGroup.groupname, app.Group{}) + AssertStatus(t, "GetGroup(AdminGroup) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "GetGroup(AdminGroup) -> result", res["error"].(error), ldap.LDAPResultStrongAuthRequired) +} + +func TestAddGetDelGroup(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + newGroup := app.Group{} + + // create new sample user group + status, _ := client.AddGroup(SampleUserGroup.groupname, newGroup) + AssertStatus(t, "AddGroup(SampleUserGroup) -> status", status, http.StatusOK) + + // try reading the new group, which should return the expected sample group with no members + status, res := client.GetGroup(SampleUserGroup.groupname) + expectedGroup := SampleUserGroup.groupObj + expectedGroup.Attributes.Member = []string{""} // override the expected members since we aren't testing that here + AssertStatus(t, "GetGroup(SampleUserGroup) -> status", status, http.StatusOK) + AssertLDAPGroupEquals(t, "GetGroup(SampleUserGroup) -> result", res["group"], expectedGroup) + + // delete the sample user group + status, _ = client.DelGroup(SampleUserGroup.groupname) + AssertStatus(t, "DelGroup(SampleUserGroup) -> status", status, http.StatusOK) + + // try reading the new group, which should return a an error since it has been deleted + status, res = client.GetGroup(SampleUserGroup.groupname) + AssertStatus(t, "GetUser(SampleUser) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "GetUser(SampleUser) -> result", res["error"].(error), ldap.LDAPResultNoSuchObject) +} + +func TestAddGroup_DuplicateGroup(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + newGroup := app.Group{} + + // create new sample user group + status, _ := client.AddGroup(SampleUserGroup.groupname, newGroup) + AssertStatus(t, "AddGroup(SampleUserGroup) -> status", status, http.StatusOK) + + // try to create new sample user again, which should fail with object already exists + status, res := client.AddGroup(SampleUserGroup.groupname, newGroup) + AssertStatus(t, "AddGroup(SampleUserGroup) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "AddGroup(SampleUserGroup) -> result", res["error"].(error), ldap.LDAPResultEntryAlreadyExists) + + // delete the sample group + status, _ = client.DelGroup(SampleUserGroup.groupname) + AssertStatus(t, "DelGroup(SampleUserGroup) -> status", status, http.StatusOK) +} + +func TestAddGroup_InsufficientPermission(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + newUser := app.UserRequired{ + CN: SampleUser.userObj.Attributes.CN, + SN: SampleUser.userObj.Attributes.SN, + Mail: SampleUser.userObj.Attributes.Mail, + UserPassword: SampleUser.password, + } + + // create new sample user, which should succeed + status, _ := client.AddUser(SampleUser.username, newUser) + AssertStatus(t, "AddUser(SampleUser) -> status", status, http.StatusOK) + + // bind as the sample user + err = client.BindUser(SampleUser.username, SampleUser.password) + AssertLDAPError(t, "BindUser(SampleUser)", err, ldap.LDAPResultSuccess) + + newGroup := app.Group{} + + // try to create a new group, which should fail with insufficient permission + status, res := client.AddGroup(InvalidGroup.groupname, newGroup) + AssertStatus(t, "AddGroup(InvalidGroup) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "AddGroup(InvalidGroup) -> result", res["error"].(error), ldap.LDAPResultInsufficientAccessRights) + + // rebind as admin user + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // delete the sample user + status, _ = client.DelUser(SampleUser.username) + AssertStatus(t, "DelUser(SampleUser) -> status", status, http.StatusOK) +} + +func TestAddGroup_NoAuth(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + newGroup := app.Group{} + + // try to create a new group, which should fail with AuthenticationRequired + status, res := client.AddGroup(InvalidGroup.groupname, newGroup) + AssertStatus(t, "AddGroup(InvalidGroup) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "AddGroup(InvalidGroup) -> result", res["error"].(error), ldap.LDAPResultStrongAuthRequired) +} + +func TestDelGroup_NoSuchGroup(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // try delete invalid group, which should fail with NoSuchObject + status, res := client.DelGroup(InvalidGroup.groupname) + AssertStatus(t, "DelGroup(InvalidGroup) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "DelGroup(InvalidGroup) -> result", res["error"].(error), ldap.LDAPResultNoSuchObject) +} + +func TestDelGroup_InsufficientPermission(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + newUser := app.UserRequired{ + CN: SampleUser.userObj.Attributes.CN, + SN: SampleUser.userObj.Attributes.SN, + Mail: SampleUser.userObj.Attributes.Mail, + UserPassword: SampleUser.password, + } + + // create new sample user, which should succeed + status, _ := client.AddUser(SampleUser.username, newUser) + AssertStatus(t, "AddUser(SampleUser) -> status", status, http.StatusOK) + + // bind as the sample user + err = client.BindUser(SampleUser.username, SampleUser.password) + AssertLDAPError(t, "BindUser(SampleUser)", err, ldap.LDAPResultSuccess) + + // try delete admin group, which should fail with InsufficientAccessRights + status, res := client.DelGroup(AdminGroup.groupname) + AssertStatus(t, "DelGroup(AdminGroup) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "DelGroup(AdminGroup) -> result", res["error"].(error), ldap.LDAPResultInsufficientAccessRights) + + // rebind as admin user + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + // delete the sample user + status, _ = client.DelUser(SampleUser.username) + AssertStatus(t, "DelUser(SampleUser) -> status", status, http.StatusOK) +} + +func TestDelGroup_NoAuth(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // test del admin group as anonymous which should fail with AuthenticationRequired + status, res := client.DelGroup(InvalidGroup.groupname) + AssertStatus(t, "DelGroup(InvalidGroup) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "DelGroup(InvalidGroup) -> result", res["error"].(error), ldap.LDAPResultStrongAuthRequired) +} + +func TestAddDelUserToGroup(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + newUser := app.UserRequired{ + CN: SampleUser.userObj.Attributes.CN, + SN: SampleUser.userObj.Attributes.SN, + Mail: SampleUser.userObj.Attributes.Mail, + UserPassword: SampleUser.password, + } + + // create new sample user, which should succeed + status, _ := client.AddUser(SampleUser.username, newUser) + AssertStatus(t, "AddUser(SampleUser) -> status", status, http.StatusOK) + + newGroup := app.Group{} + + // try to create a new group, which should succeed + status, _ = client.AddGroup(SampleUserGroup.groupname, newGroup) + AssertStatus(t, "AddGroup(SampleUserGroup) -> status", status, http.StatusOK) + + // try adding sample user to the sample user group which should succeed + status, _ = client.AddUserToGroup(SampleUser.username, SampleUserGroup.groupname) + AssertStatus(t, "AddUserToGroup(SampleUser -> SampleUserGroup) -> status", status, http.StatusOK) + + // try reading the new group, which should return the expected sample group with member + status, res := client.GetGroup(SampleUserGroup.groupname) + AssertStatus(t, "GetGroup(SampleUserGroup) -> status", status, http.StatusOK) + AssertLDAPGroupEquals(t, "GetGroup(SampleUserGroup) -> result", res["group"], SampleUserGroup.groupObj) + + // try removing sample user from the sample user group which should succeed + status, _ = client.DelUserFromGroup(SampleUser.username, SampleUserGroup.groupname) + AssertStatus(t, "DelUserFromGroup(SampleUser -> SampleUserGroup) -> status", status, http.StatusOK) + + // try reading the new group, which should return the expected sample group without any members + status, res = client.GetGroup(SampleUserGroup.groupname) + expectedGroup := SampleUserGroup.groupObj + expectedGroup.Attributes.Member = []string{""} // override the expected members since we aren't testing that here + AssertStatus(t, "GetGroup(SampleUserGroup) -> status", status, http.StatusOK) + AssertLDAPGroupEquals(t, "GetGroup(SampleUserGroup) -> result", res["group"], expectedGroup) + + // delete the sample user group + status, _ = client.DelGroup(SampleUserGroup.groupname) + AssertStatus(t, "DelGroup(SampleUserGroup) -> status", status, http.StatusOK) + + // delete the sample user + status, _ = client.DelUser(SampleUser.username) + AssertStatus(t, "DelUser(SampleUser) -> status", status, http.StatusOK) +} + +func TestAddUserToGroup_NoSuchGroup(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + newUser := app.UserRequired{ + CN: SampleUser.userObj.Attributes.CN, + SN: SampleUser.userObj.Attributes.SN, + Mail: SampleUser.userObj.Attributes.Mail, + UserPassword: SampleUser.password, + } + + // create new sample user, which should succeed + status, _ := client.AddUser(SampleUser.username, newUser) + AssertStatus(t, "AddUser(SampleUser) -> status", status, http.StatusOK) + + // try adding sample user to the sample user group which should fail with NoSuchObject + status, res := client.AddUserToGroup(SampleUser.username, SampleUserGroup.groupname) + AssertStatus(t, "AddUserToGroup(SampleUser -> SampleUserGroup) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "AddUserToGroup(InvalidGroup) -> result", res["error"].(error), ldap.LDAPResultNoSuchObject) + + // delete the sample user + status, _ = client.DelUser(SampleUser.username) + AssertStatus(t, "DelUser(SampleUser) -> status", status, http.StatusOK) +} + +func TestDelUserFromGroup_NoSuchGroup(t *testing.T) { + // create client + config, err := app.GetConfig("test_config.json") + AssertError(t, "GetConfig()", err, nil) + client, err := app.NewLDAPClient(config) + AssertLDAPError(t, "NewLDAPClient()", err, ldap.LDAPResultSuccess) + + // bind using admin user credentials which should succeed + err = client.BindUser(AdminUser.username, AdminUser.password) + AssertLDAPError(t, "BindUser(AdminUser)", err, ldap.LDAPResultSuccess) + + newUser := app.UserRequired{ + CN: SampleUser.userObj.Attributes.CN, + SN: SampleUser.userObj.Attributes.SN, + Mail: SampleUser.userObj.Attributes.Mail, + UserPassword: SampleUser.password, + } + + // create new sample user, which should succeed + status, _ := client.AddUser(SampleUser.username, newUser) + AssertStatus(t, "AddUser(SampleUser) -> status", status, http.StatusOK) + + // try adding sample user to the sample user group which should fail with NoSuchObject + status, res := client.DelUserFromGroup(SampleUser.username, SampleUserGroup.groupname) + AssertStatus(t, "DelUserFromGroup(SampleUser -> SampleUserGroup) -> status", status, http.StatusBadRequest) + AssertLDAPError(t, "DelUserFromGroup(InvalidGroup) -> result", res["error"].(error), ldap.LDAPResultNoSuchObject) + + // delete the sample user + status, _ = client.DelUser(SampleUser.username) + AssertStatus(t, "DelUser(SampleUser) -> status", status, http.StatusOK) +} diff --git a/test/test_config.json b/test/test_config.json new file mode 100644 index 0000000..243b83f --- /dev/null +++ b/test/test_config.json @@ -0,0 +1,14 @@ +{ + "listenPort": 80, + "ldapURL": "ldap://localhost", + "startTLS": true, + "basedn": "dc=test,dc=paasldap", + "sessionSecretKey": "test", + "sessionCookieName": "PAASLDAPAuthTicket", + "sessionCookie": { + "path": "/", + "httpOnly": true, + "secure": false, + "maxAge": 7200 + } +} \ No newline at end of file diff --git a/test/test_utils.go b/test/test_utils.go new file mode 100644 index 0000000..f1599e9 --- /dev/null +++ b/test/test_utils.go @@ -0,0 +1,156 @@ +package tests + +import ( + "fmt" + "math/rand" + "net/http" + "proxmoxaas-ldap/app" + "reflect" + "testing" + + "github.com/gin-gonic/gin" + "github.com/go-ldap/ldap/v3" +) + +func RandInt(min int, max int) int { + return rand.Intn(max+1-min) + min +} + +func RandString(n int) string { + var letters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} + +// random ldap style DN +func RandDN(n int) string { + return fmt.Sprintf("cn=%s,ou=%s,dc=%s,dc=%s", RandString(n), RandString(n), RandString(n), RandString(n)) +} + +// typically for testing values of a variable +func AssertEquals[T comparable](t *testing.T, label string, a T, b T) { + t.Helper() + if a != b { + t.Errorf(`%s = %#v; expected %#v.`, label, a, b) + } +} + +// asserting the success or failure of a generic error +func AssertError(t *testing.T, label string, gotErr error, expectErr error) { + t.Helper() + if gotErr != nil && expectErr != nil { + if gotErr.Error() != expectErr.Error() { + t.Errorf(`%s returned %s; expected %s`, label, gotErr.Error(), expectErr.Error()) + } + } else { + if gotErr != expectErr { + t.Errorf(`%s returned %s; expected %s`, label, gotErr.Error(), expectErr.Error()) + } + } +} + +// typically for asserting the success or failure of an ldap result +func AssertLDAPError(t *testing.T, label string, gotErr any, expectErrCode uint16) { + t.Helper() + expectError := ldap.LDAPResultCodeMap[expectErrCode] + if expectErrCode == ldap.LDAPResultSuccess { // expect success + if gotErr != nil { // got an error + gotErr := gotErr.(error) + t.Errorf(`%s returned %s; expected %s.`, label, gotErr.Error(), "success") + } // did not get an error + } else { // expect error + if gotErr == nil { // did not get an error + t.Errorf(`%s returned %s; expected %s.`, label, "success", expectError) + return + } + gotErr := gotErr.(error) + if !ldap.IsErrorWithCode(gotErr, expectErrCode) { // got an error that does not match the expected error + t.Errorf(`%s returned %s; expected %s.`, label, gotErr.Error(), expectError) + } // got the expected error + } +} + +// typically for asserting the success or failure of an http result +func AssertStatus(t *testing.T, label string, gotCode int, expectCode int) { + t.Helper() + if expectCode == http.StatusOK { + if gotCode != http.StatusOK { // got an error + t.Errorf(`%s returned %d; expected %d.`, label, gotCode, expectCode) + } + } else { // expect error + if gotCode == http.StatusOK { // did not get an error + t.Errorf(`%s returned %d; expected %d.`, label, gotCode, expectCode) + } else if gotCode != expectCode { // got an error that does not match the expected error + t.Errorf(`%s returned %d; expected %d.`, label, gotCode, expectCode) + } + } +} + +// compare if two users are equal, accepts LDAPUser or gin.H +func AssertLDAPUserEquals(t *testing.T, label string, a any, b app.LDAPUser) { + t.Helper() + + aObj, ok := a.(app.LDAPUser) + if ok { + if !reflect.DeepEqual(aObj, b) { + t.Errorf(`%s = %#v; expected %#v.`, label, aObj, b) + } + return + } + + aGin, ok := a.(gin.H) + if ok { + bGin := app.LDAPUserToGin(b) + if !reflect.DeepEqual(aGin, bGin) { + t.Errorf(`%s = %#v; expected %#v.`, label, aGin, bGin) + } + return + } + + // not a supported type + t.Errorf(`%s = %#v; expected %#v.`, label, a, b) +} + +// compare if two users are equal, accepts LDAPUser or gin.H +func AssertLDAPGroupEquals(t *testing.T, label string, a any, b app.LDAPGroup) { + t.Helper() + + aObj, ok := a.(app.LDAPGroup) + if ok { + if !reflect.DeepEqual(aObj, b) { + t.Errorf(`%s = %#v; expected %#v.`, label, aObj, b) + } + return + } + + aGin, ok := a.(gin.H) + if ok { + bGin := app.LDAPGroupToGin(b) + if !reflect.DeepEqual(aGin, bGin) { + t.Errorf(`%s = %#v; expected %#v.`, label, aGin, bGin) + } + return + } + + // not a supported type + t.Errorf(`%s = %#v; expected %#v.`, label, a, b) +} + +var _config, _ = app.GetConfig("test_config.json") +var BaseDN = _config.BaseDN +var PeopleDN = fmt.Sprintf("ou=people,%s", BaseDN) +var GroupDN = fmt.Sprintf("ou=groups,%s", BaseDN) + +type User struct { + username string + password string + userObj app.LDAPUser +} + +type Group struct { + groupname string + groupObj app.LDAPGroup +} diff --git a/test/unit_test.go b/test/unit_test.go new file mode 100644 index 0000000..ea9504d --- /dev/null +++ b/test/unit_test.go @@ -0,0 +1,126 @@ +package tests + +import ( + "errors" + "fmt" + app "proxmoxaas-ldap/app" + "testing" + + "github.com/gin-gonic/gin" + "github.com/go-ldap/ldap/v3" +) + +// test the GetConfig utility function because it used in other tests +func TestConfig_ValidPath(t *testing.T) { + config, err := app.GetConfig("test_config.json") + + AssertError(t, "GetConfig()", err, nil) + AssertEquals(t, "config.ListenPort", config.ListenPort, 80) + AssertEquals(t, "config.LdapURL", config.LdapURL, "ldap://localhost") + AssertEquals(t, "config.BaseDN", config.BaseDN, "dc=test,dc=paasldap") + AssertEquals(t, "config.SessionSecretKey", config.SessionSecretKey, "test") + AssertEquals(t, "config.SessionCookieName", config.SessionCookieName, "PAASLDAPAuthTicket") + AssertEquals(t, "config.SessionCookie.Path", config.SessionCookie.Path, "/") + AssertEquals(t, "config.SessionCookie.HttpOnly", config.SessionCookie.HttpOnly, true) + AssertEquals(t, "config.SessionCookie.Secure", config.SessionCookie.Secure, false) + AssertEquals(t, "config.SessionCookie.MaxAge", config.SessionCookie.MaxAge, 7200) +} + +func TestConfig_InvalidPath(t *testing.T) { + badFileName := RandString(16) + _, err := app.GetConfig(badFileName) + expectedErr := fmt.Errorf("open %s: no such file or directory", badFileName) + AssertError(t, "GetConfig()", err, expectedErr) + + _, err = app.GetConfig("bad_config.json") + expectedErr = fmt.Errorf("invalid character ',' looking for beginning of object key string") + AssertError(t, "GetConfig()", err, expectedErr) +} + +// test the LDAPEntryToUser and LDAPUserToGin utility functions +func TestLDAPUserDataPipeline(t *testing.T) { + var memberOf []string + for i := 0; i < RandInt(5, 20); i++ { + memberOf = append(memberOf, RandDN(16)) + } + + expectedUser := app.LDAPUser{ + DN: RandDN(16), + Attributes: app.LDAPUserAttributes{ + CN: RandString(16), + SN: RandString(16), + Mail: RandString(16), + UID: RandString(16), + MemberOf: memberOf, + }, + } + + attributes := make(map[string][]string) + attributes["cn"] = []string{expectedUser.Attributes.CN} + attributes["sn"] = []string{expectedUser.Attributes.SN} + attributes["mail"] = []string{expectedUser.Attributes.Mail} + attributes["uid"] = []string{expectedUser.Attributes.UID} + attributes["memberOf"] = expectedUser.Attributes.MemberOf + + entry := ldap.NewEntry(expectedUser.DN, attributes) + + user := app.LDAPEntryToLDAPUser(entry) + AssertLDAPUserEquals(t, "LDAPEntryToLDAPUser(entry) -> user", user, expectedUser) + + json := app.LDAPUserToGin(user) + AssertLDAPUserEquals(t, "LDAPUserToGin(user) -> json", json, expectedUser) +} + +// test the LDAPEntryToGroup and LDAPGroupToGin utility functions +func TestLDAPGroupDataPipeline(t *testing.T) { + var member []string + for i := 0; i < RandInt(5, 20); i++ { + member = append(member, RandDN(16)) + } + + expectedGroup := app.LDAPGroup{ + DN: RandDN(16), + Attributes: app.LDAPGroupAttributes{ + Member: member, + }, + } + + attributes := make(map[string][]string) + attributes["member"] = expectedGroup.Attributes.Member + + entry := ldap.NewEntry(expectedGroup.DN, attributes) + + group := app.LDAPEntryToLDAPGroup(entry) + AssertLDAPGroupEquals(t, "LDAPEntryToLDAPGroup(entry) -> group", group, expectedGroup) + + json := app.LDAPGroupToGin(group) + AssertLDAPGroupEquals(t, "LDAPGroupToGin(group) -> json", json, expectedGroup) +} + +func TestHandleResponse(t *testing.T) { + for errorCode := range ldap.LDAPResultCodeMap { + expectedMessage := RandString(16) + LDAPerr := ldap.NewError(errorCode, errors.New(expectedMessage)) + res := gin.H{ + "error": LDAPerr, + } + LDAPResult := ldap.LDAPResultCodeMap[errorCode] + + handledResponseError := (app.HandleResponse(res))["error"].(gin.H) + + AssertEquals(t, `HandleResponse(res)["error"]["code"]`, handledResponseError["code"].(uint16), errorCode) + AssertEquals(t, `HandleResponse(res)["error"]["result"]`, handledResponseError["result"].(string), LDAPResult) + AssertEquals(t, `HandleResponse(res)["error"]["message"]`, handledResponseError["message"].(string), expectedMessage) + } + + res := gin.H{ + "ok": true, + "status": RandInt(0, 600), + } + + handledResponse := app.HandleResponse(res) + + AssertEquals(t, `HandleResponse(res)["ok"]`, handledResponse["ok"].(bool), res["ok"].(bool)) + AssertEquals(t, `HandleResponse(res)["satus"]`, handledResponse["status"].(int), res["status"].(int)) + AssertEquals(t, `HandleResponse(res)["error"]`, handledResponse["error"], nil) +} From 0d1dd540c0fc010352871dcf5ead0a99d1940f48 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Mon, 21 Oct 2024 19:56:01 +0000 Subject: [PATCH 16/24] update go.mod --- go.mod | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 5e3866a..f6cb8b4 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module proxmoxaas-ldap -go 1.23 - -toolchain go1.23.2 +go 1.23.2 require ( github.com/gin-contrib/sessions v1.0.1 @@ -14,7 +12,7 @@ require ( require ( github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/bytedance/sonic v1.12.3 // indirect - github.com/bytedance/sonic/loader v0.2.0 // indirect + github.com/bytedance/sonic/loader v0.2.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.6 // indirect @@ -31,6 +29,7 @@ require ( github.com/gorilla/sessions v1.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/knz/go-libedit v1.10.1 // 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 From 162eda70b9d907be24d40a46c9dcde484516e107 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Wed, 23 Oct 2024 18:58:06 +0000 Subject: [PATCH 17/24] fix make PHONY targets --- Makefile | 2 +- scripts/Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index ba6e5f1..87fbb73 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build test clean dev-init +.PHONY: build tests clean dev-init dev-reinit build: clean @echo "======================== Building Binary =======================" diff --git a/scripts/Makefile b/scripts/Makefile index fccee96..b17f665 100644 --- a/scripts/Makefile +++ b/scripts/Makefile @@ -1,4 +1,4 @@ -.PHONY: dev-init +.PHONY: prerequisites dev-init dev-reinit prerequisites: @echo "=================== Installing Prerequisites ===================" From e18737c043652a9f51d402e68654a5912671afa0 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Wed, 23 Oct 2024 22:46:43 +0000 Subject: [PATCH 18/24] cleanup gitignore --- .gitignore | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 4290158..2d95b16 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -**/go.sum -**/config.json -dist/* \ No newline at end of file +go.sum +dist/* +**/config.json \ No newline at end of file From bf809451681b0f72f9f986a71f61bbcea7b2a288 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Thu, 24 Oct 2024 18:08:43 +0000 Subject: [PATCH 19/24] rename make testsd target to test --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 87fbb73..e72b3ca 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ -.PHONY: build tests clean dev-init dev-reinit +.PHONY: build test clean dev-init dev-reinit build: clean @echo "======================== Building Binary =======================" CGO_ENABLED=0 go build -ldflags="-s -w" -v -o dist/ . -tests: dev-reinit +test: dev-reinit @echo "======================== Running Tests =========================" go test -v -cover -coverpkg=./app/ -coverprofile coverage ./test/ @echo "======================= Coverage Report ========================" From 7da5c223136ae1195c87b95a3846b2640b9ad048 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Thu, 14 Nov 2024 07:26:45 +0000 Subject: [PATCH 20/24] update go.mod --- go.mod | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index f6cb8b4..763cde9 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( require ( github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect - github.com/bytedance/sonic v1.12.3 // indirect + github.com/bytedance/sonic v1.12.4 // indirect github.com/bytedance/sonic/loader v0.2.1 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect @@ -28,7 +28,7 @@ require ( github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/knz/go-libedit v1.10.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -37,11 +37,11 @@ require ( github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect - golang.org/x/arch v0.11.0 // indirect - golang.org/x/crypto v0.28.0 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/arch v0.12.0 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) From 33566572fb381ba4a166c2953caa0b2a3e4dccd6 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Fri, 15 Nov 2024 00:35:57 +0000 Subject: [PATCH 21/24] generate session secret key randomly each application start, bump app version 1.0.6 --- app/app.go | 19 +++++++++++++++---- app/utils.go | 1 - configs/template.config.json | 1 - test/test_config.json | 1 - test/unit_test.go | 1 - 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/app.go b/app/app.go index a729329..f28adf8 100644 --- a/app/app.go +++ b/app/app.go @@ -1,6 +1,7 @@ package app import ( + "crypto/rand" "encoding/gob" "flag" "log" @@ -15,7 +16,7 @@ import ( ) var LDAPSessions map[string]*LDAPClient -var AppVersion = "1.0.5" +var AppVersion = "1.0.6" var APIVersion = "1.0.4" func Run() { @@ -28,13 +29,20 @@ func Run() { config, err := GetConfig(*configPath) if err != nil { - log.Fatal("Error when reading config file: ", err) + log.Fatalf("Error when reading config file: %s\n", err) } log.Printf("Read in config from %s\n", *configPath) + secretKey := make([]byte, 256) + n, err := rand.Read(secretKey) + if err != nil { + log.Fatalf("Error when generating session secret key: %s\n", err.Error()) + } + log.Printf("Generated session secret key of length %d\n", n) + gin.SetMode(gin.ReleaseMode) router := gin.Default() - store := cookie.NewStore([]byte(config.SessionSecretKey)) + store := cookie.NewStore(secretKey) store.Options(sessions.Options{ Path: config.SessionCookie.Path, HttpOnly: config.SessionCookie.HttpOnly, @@ -310,5 +318,8 @@ func Run() { log.Printf("Starting LDAP API on port %s\n", strconv.Itoa(config.ListenPort)) - router.Run("0.0.0.0:" + strconv.Itoa(config.ListenPort)) + err = router.Run("0.0.0.0:" + strconv.Itoa(config.ListenPort)) + if err != nil { + log.Fatalf("Error starting router: %s", err.Error()) + } } diff --git a/app/utils.go b/app/utils.go index d1b9e71..4643689 100644 --- a/app/utils.go +++ b/app/utils.go @@ -13,7 +13,6 @@ type Config struct { LdapURL string `json:"ldapURL"` StartTLS bool `json:"startTLS"` BaseDN string `json:"baseDN"` - SessionSecretKey string `json:"sessionSecretKey"` SessionCookieName string `json:"sessionCookieName"` SessionCookie struct { Path string `json:"path"` diff --git a/configs/template.config.json b/configs/template.config.json index cfda43d..585b129 100644 --- a/configs/template.config.json +++ b/configs/template.config.json @@ -3,7 +3,6 @@ "ldapURL": "ldap://localhost", "startTLS": true, "basedn": "dc=example,dc=com", - "sessionSecretKey": "super secret key", "sessionCookieName": "PAASLDAPAuthTicket", "sessionCookie": { "path": "/", diff --git a/test/test_config.json b/test/test_config.json index 243b83f..5dc2ded 100644 --- a/test/test_config.json +++ b/test/test_config.json @@ -3,7 +3,6 @@ "ldapURL": "ldap://localhost", "startTLS": true, "basedn": "dc=test,dc=paasldap", - "sessionSecretKey": "test", "sessionCookieName": "PAASLDAPAuthTicket", "sessionCookie": { "path": "/", diff --git a/test/unit_test.go b/test/unit_test.go index ea9504d..1142b8f 100644 --- a/test/unit_test.go +++ b/test/unit_test.go @@ -18,7 +18,6 @@ func TestConfig_ValidPath(t *testing.T) { AssertEquals(t, "config.ListenPort", config.ListenPort, 80) AssertEquals(t, "config.LdapURL", config.LdapURL, "ldap://localhost") AssertEquals(t, "config.BaseDN", config.BaseDN, "dc=test,dc=paasldap") - AssertEquals(t, "config.SessionSecretKey", config.SessionSecretKey, "test") AssertEquals(t, "config.SessionCookieName", config.SessionCookieName, "PAASLDAPAuthTicket") AssertEquals(t, "config.SessionCookie.Path", config.SessionCookie.Path, "/") AssertEquals(t, "config.SessionCookie.HttpOnly", config.SessionCookie.HttpOnly, true) From 222864868d350ece2c9ff8eb9cce90c78f4b87e2 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Fri, 15 Nov 2024 00:47:29 +0000 Subject: [PATCH 22/24] update README --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8f7138c..48e4a95 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,16 @@ ProxmoxAAS LDAP provides a simple API for managing users and groups in a simplif 1. Download `proxmoxaas-ldap` binary and `template.config.json` file from [releases](https://git.tronnet.net/tronnet/ProxmoxAAS-LDAP/releases) 2. Rename `template.config.json` to `config.json` and modify: + - listenPort: port for PAAS-LDAP to bind and listen on - ldapURL: url to the ldap server ie. `ldap://ldap.domain.net` - - baseDN: base DN ie. `dc=domain,dc=net` - - sessionSecretKey: random value used to randomize cookie values, replace with any sufficiently large random string + - startTLS: true if backend LDAP supports StartTLS + - basedn: base DN ie. `dc=domain,dc=net` + - sessionCookieName: name of the session cookie + - sessionCookie: specific cookie properties + - path: cookie path + - httpOnly: cookie http-only + - secure: cookie secure + - maxAge: cookie max-age 3. Run the binary ## Building and Testing from Source @@ -50,4 +57,4 @@ Building requires the go toolchain. Testing requires the go toolchain, make, and 1. Clone the repository 2. Run `go get` to get requirements 3. Run `make dev-init` to install test requirements including openldap (slapd), ldap-utils, debconf-utils -4. Run `make tests` to run all tests \ No newline at end of file +4. Run `make test` to run all tests \ No newline at end of file From 105b11cae567a610f533f9d926489dc66e0c9da7 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Wed, 15 Jan 2025 05:58:12 +0000 Subject: [PATCH 23/24] add missing prerequisite gnutls-bin to dev-init make target --- app/app.go | 2 +- scripts/Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/app.go b/app/app.go index f28adf8..4da080f 100644 --- a/app/app.go +++ b/app/app.go @@ -21,6 +21,7 @@ var APIVersion = "1.0.4" func Run() { gob.Register(LDAPClient{}) + gin.SetMode(gin.ReleaseMode) log.Printf("Starting ProxmoxAAS-LDAP version %s\n", APIVersion) @@ -40,7 +41,6 @@ func Run() { } log.Printf("Generated session secret key of length %d\n", n) - gin.SetMode(gin.ReleaseMode) router := gin.Default() store := cookie.NewStore(secretKey) store.Options(sessions.Options{ diff --git a/scripts/Makefile b/scripts/Makefile index b17f665..9728481 100644 --- a/scripts/Makefile +++ b/scripts/Makefile @@ -2,7 +2,7 @@ prerequisites: @echo "=================== Installing Prerequisites ===================" - apt install debconf-utils slapd ldap-utils sudo gettext + apt install debconf-utils slapd ldap-utils sudo gettext gnutls-bin git clone https://git.tronnet.net/tronnet/open-ldap-setup cd open-ldap-setup/; bash gencert.sh < ../gencert.conf; rm -rf open-ldap-setup/ From 929c381cc85fc1b8e7efaa42e850e168c088d573 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Tue, 11 Feb 2025 06:54:15 +0000 Subject: [PATCH 24/24] update go version, update dependencies --- go.mod | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 763cde9..c0e0ae9 100644 --- a/go.mod +++ b/go.mod @@ -1,27 +1,27 @@ module proxmoxaas-ldap -go 1.23.2 +go 1.23.6 require ( - github.com/gin-contrib/sessions v1.0.1 + github.com/gin-contrib/sessions v1.0.2 github.com/gin-gonic/gin v1.10.0 - github.com/go-ldap/ldap/v3 v3.4.8 + github.com/go-ldap/ldap/v3 v3.4.10 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.12.4 // indirect - github.com/bytedance/sonic/loader v0.2.1 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect + github.com/bytedance/sonic v1.12.8 // indirect + github.com/bytedance/sonic/loader v0.2.3 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/iasm v0.2.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.6 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.0.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.1 // indirect - github.com/goccy/go-json v0.10.3 // indirect + github.com/go-playground/validator/v10 v10.24.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/context v1.1.2 // indirect @@ -37,11 +37,11 @@ require ( github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect - golang.org/x/arch v0.12.0 // indirect - golang.org/x/crypto v0.29.0 // indirect - golang.org/x/net v0.31.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/text v0.20.0 // indirect - google.golang.org/protobuf v1.35.1 // indirect + golang.org/x/arch v0.14.0 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect )