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) +}