// Copyright (c) 2015-2022 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package ldap

import (
	"fmt"
	"os"
	"testing"

	"github.com/minio/minio-go/v7/pkg/set"
)

const (
	EnvTestLDAPServer = "LDAP_TEST_SERVER"
)

func TestConfigValidator(t *testing.T) {
	ldapServer := os.Getenv(EnvTestLDAPServer)
	if ldapServer == "" {
		t.Logf("Skipping test as %s is not set", EnvTestLDAPServer)
		t.Skip()
	}
	testCases := []struct {
		cfg            Config
		expectedResult Result
	}{
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				return v
			}(),
			expectedResult: ConnectionParamMisconfigured,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = ldapServer
				return v
			}(),
			expectedResult: ConnectivityError,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = ldapServer
				v.ServerInsecure = true
				return v
			}(),
			expectedResult: LookupBindError,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = ldapServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin1"
				return v
			}(),
			expectedResult: LookupBindError,
		},
		{ // Case 4
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = ldapServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				return v
			}(),
			expectedResult: UserSearchParamsMisconfigured,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = ldapServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=x)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io"
				return v
			}(),
			expectedResult: UserSearchParamsMisconfigured,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = ldapServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io"
				return v
			}(),
			expectedResult: ConfigOk,
		},
		{ // Case 7
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = ldapServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io"
				v.GroupSearchBaseDistName = "ou=swengg,dc=min,dc=io"
				v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=x))"
				return v
			}(),
			expectedResult: GroupSearchParamsMisconfigured,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = ldapServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io"
				v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=x))"
				return v
			}(),
			expectedResult: GroupSearchParamsMisconfigured,
		},
		{ // Case 9
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = ldapServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io"
				v.GroupSearchBaseDistName = "ou=swengg,dc=min,dc=io"
				v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=%d))"
				return v
			}(),
			expectedResult: ConfigOk,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = ldapServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "min,dc=io" // not a valid DN
				v.GroupSearchBaseDistName = "ou=swengg,dc=min,dc=io"
				v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=%d))"
				return v
			}(),
			expectedResult: UserSearchParamsMisconfigured,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = ldapServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "uid=%s" // not a valid filter
				v.UserDNSearchBaseDistName = "dc=min,dc=io"
				v.GroupSearchBaseDistName = "ou=swengg,dc=min,dc=io"
				v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=%d))"
				return v
			}(),
			expectedResult: UserSearchParamsMisconfigured,
		},
		{ // Case 12
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = ldapServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io"
				v.GroupSearchBaseDistName = "a" // Not a valid DN
				v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=%d))"
				return v
			}(),
			expectedResult: GroupSearchParamsMisconfigured,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = ldapServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io"
				v.GroupSearchBaseDistName = "ou=swengg,dc=min,dc=io"
				v.GroupSearchFilter = "member=%d" // Invalid search filter
				return v
			}(),
			expectedResult: GroupSearchParamsMisconfigured,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = ldapServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io;adcio" // Invalid DN (multiple)
				v.GroupSearchBaseDistName = "ou=swengg,dc=min,dc=io"
				v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=%d))"
				return v
			}(),
			expectedResult: UserSearchParamsMisconfigured,
		},
		{ // Case 15
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = ldapServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io;dc=io" // Overlapping subtrees
				v.GroupSearchBaseDistName = "ou=swengg,dc=min,dc=io"
				v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=%d))"
				return v
			}(),
			expectedResult: UserSearchParamsMisconfigured,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = ldapServer
				v.SRVRecordName = "thingy" // Bad SRV record name.
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io"
				v.GroupSearchBaseDistName = "ou=swengg,dc=min,dc=io"
				v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=%d))"
				return v
			}(),
			expectedResult: ConnectionParamMisconfigured,
		},
	}

	expectedDN := "uid=dillon,ou=people,ou=swengg,dc=min,dc=io"
	expectedGroups := set.CreateStringSet(
		"cn=projecta,ou=groups,ou=swengg,dc=min,dc=io",
		"cn=projectb,ou=groups,ou=swengg,dc=min,dc=io",
		"cn=project/d,ou=groups,ou=swengg,dc=min,dc=io",
	)

	for i, test := range testCases {
		result := test.cfg.Validate()
		if result.Result != test.expectedResult {
			fmt.Printf("Result: %#v\n", result)
			t.Fatalf("Case %d: Got `%s` expected `%s`", i, result.Result, string(test.expectedResult))
		}
		if result.IsOk() {
			lookupResult, validationResult := test.cfg.ValidateLookup("dillon")
			if !validationResult.IsOk() {
				t.Fatalf("Case %d: Got unexpected validation failure: %#v\n", i, validationResult)
			}
			if lookupResult.DN != expectedDN {
				t.Fatalf("Case %d: Got unexpected DN: %v", i, lookupResult.DN)
			}

			if test.cfg.GroupSearchFilter == "" {
				continue
			}

			if set.CreateStringSet(lookupResult.GroupDNMemberships...).Intersection(expectedGroups).IsEmpty() {
				t.Fatalf("Case %d: Got unexpected groups: %v", i, lookupResult.GroupDNMemberships)
			}
		}
	}
}

func TestOfflineValidator(t *testing.T) {
	fakeLDAPServer := "offlinehost:389"
	testCases := []struct {
		cfg            Config
		expectedResult Result
	}{
		{ // Case 4
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = fakeLDAPServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				return v
			}(),
			expectedResult: UserSearchParamsMisconfigured,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = fakeLDAPServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=x)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io"
				return v
			}(),
			expectedResult: UserSearchParamsMisconfigured,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = fakeLDAPServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io"
				return v
			}(),
			expectedResult: ConfigOk,
		},
		{ // Case 7
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = fakeLDAPServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io"
				v.GroupSearchBaseDistName = "ou=swengg,dc=min,dc=io"
				v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=x))"
				return v
			}(),
			expectedResult: GroupSearchParamsMisconfigured,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = fakeLDAPServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io"
				v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=x))"
				return v
			}(),
			expectedResult: GroupSearchParamsMisconfigured,
		},
		{ // Case 9
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = fakeLDAPServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io"
				v.GroupSearchBaseDistName = "ou=swengg,dc=min,dc=io"
				v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=%d))"
				return v
			}(),
			expectedResult: ConfigOk,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = fakeLDAPServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "min,dc=io" // not a valid DN
				v.GroupSearchBaseDistName = "ou=swengg,dc=min,dc=io"
				v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=%d))"
				return v
			}(),
			expectedResult: UserSearchParamsMisconfigured,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = fakeLDAPServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "uid=%s" // not a valid filter
				v.UserDNSearchBaseDistName = "dc=min,dc=io"
				v.GroupSearchBaseDistName = "ou=swengg,dc=min,dc=io"
				v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=%d))"
				return v
			}(),
			expectedResult: UserSearchParamsMisconfigured,
		},
		{ // Case 12
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = fakeLDAPServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io"
				v.GroupSearchBaseDistName = "a" // Not a valid DN
				v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=%d))"
				return v
			}(),
			expectedResult: GroupSearchParamsMisconfigured,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = fakeLDAPServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io"
				v.GroupSearchBaseDistName = "ou=swengg,dc=min,dc=io"
				v.GroupSearchFilter = "member=%d" // Invalid search filter
				return v
			}(),
			expectedResult: GroupSearchParamsMisconfigured,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = fakeLDAPServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io;adcio" // Invalid DN (multiple)
				v.GroupSearchBaseDistName = "ou=swengg,dc=min,dc=io"
				v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=%d))"
				return v
			}(),
			expectedResult: UserSearchParamsMisconfigured,
		},
		{ // Case 15
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = fakeLDAPServer
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io;dc=io" // Overlapping subtrees
				v.GroupSearchBaseDistName = "ou=swengg,dc=min,dc=io"
				v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=%d))"
				return v
			}(),
			expectedResult: UserSearchParamsMisconfigured,
		},
		{
			cfg: func() Config {
				v := Config{Enabled: true}
				v.ServerAddr = fakeLDAPServer
				v.SRVRecordName = "thingy" // Bad SRV record name.
				v.ServerInsecure = true
				v.LookupBindDN = "cn=admin,dc=min,dc=io"
				v.LookupBindPassword = "admin"
				v.UserDNSearchFilter = "(uid=%s)"
				v.UserDNSearchBaseDistName = "dc=min,dc=io"
				v.GroupSearchBaseDistName = "ou=swengg,dc=min,dc=io"
				v.GroupSearchFilter = "(&(objectclass=groupofnames)(member=%d))"
				return v
			}(),
			expectedResult: ConnectionParamMisconfigured,
		},
	}

	for i, test := range testCases {
		result := test.cfg.ValidateOffline()
		if result.Result != test.expectedResult {
			t.Fatalf("Case %d: Got `%s` expected `%s`", i, result.Result, string(test.expectedResult))
		}
	}
}
