// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
package groovy.org.apache.cloudstack.ldap

import com.cloud.user.Account
import com.cloud.user.AccountManager
import com.cloud.user.User
import com.cloud.user.UserAccount
import com.cloud.user.UserAccountVO
import com.cloud.user.dao.UserAccountDao
import com.cloud.utils.Pair
import org.apache.cloudstack.auth.UserAuthenticator
import org.apache.cloudstack.ldap.LdapAuthenticator
import org.apache.cloudstack.ldap.LdapManager
import org.apache.cloudstack.ldap.LdapTrustMapVO
import org.apache.cloudstack.ldap.LdapUser

class LdapAuthenticatorSpec extends spock.lang.Specification {

    def "Test a failed authentication due to user not being found within cloudstack"() {
        given: "We have an LdapManager, userAccountDao and ldapAuthenticator and the user doesn't exist within cloudstack."
        LdapManager ldapManager = Mock(LdapManager)
        UserAccountDao userAccountDao = Mock(UserAccountDao)
        userAccountDao.getUserAccount(_, _) >> null
        def ldapAuthenticator = new LdapAuthenticator(ldapManager, userAccountDao)
        when: "A user authentications"
        def result = ldapAuthenticator.authenticate("rmurphy", "password", 0, null)
        then: "their authentication fails"
        result.first() == false
    }

    def "Test failed authentication due to ldap bind being unsuccessful"() {
        given: "We have an LdapManager, LdapConfiguration, userAccountDao and LdapAuthenticator"
        def ldapManager = Mock(LdapManager)
        def ldapUser = Mock(LdapUser)
        ldapUser.isDisabled() >> false
        ldapManager.isLdapEnabled() >> true
        ldapManager.getUser("rmurphy", null) >> ldapUser
        ldapManager.canAuthenticate(_, _, _) >> false

        UserAccountDao userAccountDao = Mock(UserAccountDao)
        userAccountDao.getUserAccount(_, _) >> new UserAccountVO()
        def ldapAuthenticator = new LdapAuthenticator(ldapManager, userAccountDao)

        when: "The user authenticates with an incorrect password"
        def result = ldapAuthenticator.authenticate("rmurphy", "password", 0, null)

        then: "their authentication fails"
        result.first() == false
    }

    def "Test failed authentication due to ldap not being configured"() {
        given: "We have an LdapManager, A configured LDAP server, a userAccountDao and LdapAuthenticator"
        def ldapManager = Mock(LdapManager)
        ldapManager.isLdapEnabled() >> false

        UserAccountDao userAccountDao = Mock(UserAccountDao)
        userAccountDao.getUserAccount(_, _) >> new UserAccountVO()

        def ldapAuthenticator = new LdapAuthenticator(ldapManager, userAccountDao)
        when: "The user authenticates"
        def result = ldapAuthenticator.authenticate("rmurphy", "password", 0, null)
        then: "their authentication fails"
        result.first() == false
    }

    def "Test successful authentication"() {
        given: "We have an LdapManager, LdapConfiguration, userAccountDao and LdapAuthenticator"
        def ldapManager = Mock(LdapManager)
        def ldapUser = Mock(LdapUser)
        ldapUser.isDisabled() >> false
        ldapManager.isLdapEnabled() >> true
        ldapManager.canAuthenticate(_, _, _) >> true
        ldapManager.getUser("rmurphy", null) >> ldapUser

        UserAccountDao userAccountDao = Mock(UserAccountDao)
        userAccountDao.getUserAccount(_, _) >> new UserAccountVO()
        def ldapAuthenticator = new LdapAuthenticator(ldapManager, userAccountDao)

        when: "The user authenticates with an incorrect password"
        def result = ldapAuthenticator.authenticate("rmurphy", "password", 0, null)

        then: "their authentication passes"
        result.first() == true
    }

    def "Test that encode doesn't change the input"() {
        given: "We have an LdapManager, userAccountDao and LdapAuthenticator"
        LdapManager ldapManager = Mock(LdapManager)
        UserAccountDao userAccountDao = Mock(UserAccountDao)
        def ldapAuthenticator = new LdapAuthenticator(ldapManager, userAccountDao)
        when: "a users password is encoded"
        def result = ldapAuthenticator.encode("password")
        then: "it doesn't change"
        result == "password"
    }

    def "test authentication when ldap is disabled"(){
        LdapManager ldapManager = Mock(LdapManager)
        UserAccountDao userAccountDao = Mock(UserAccountDao)
        def ldapAuthenticator = new LdapAuthenticator(ldapManager, userAccountDao)
        ldapManager.isLdapEnabled() >> false

        when:
            Pair<Boolean, UserAuthenticator.ActionOnFailedAuthentication> result = ldapAuthenticator.authenticate("rajanik", "password", 1, null)
        then:
            result.first() == false
            result.second() == null

    }

    // tests when domain is linked to LDAP
    def "test authentication when domain is linked and user disabled in ldap"(){
        LdapManager ldapManager = Mock(LdapManager)
        UserAccountDao userAccountDao = Mock(UserAccountDao)
        AccountManager accountManager = Mock(AccountManager)

        def ldapAuthenticator = new LdapAuthenticator()
        ldapAuthenticator._ldapManager = ldapManager
        ldapAuthenticator._userAccountDao = userAccountDao
        ldapAuthenticator._accountManager = accountManager

        long domainId = 1;
        String username = "rajanik"
        LdapManager.LinkType type = LdapManager.LinkType.GROUP
        String name = "CN=test,DC=ccp,DC=citrix,DC=com"

        ldapManager.isLdapEnabled() >> true
        UserAccount userAccount = Mock(UserAccount)
        userAccountDao.getUserAccount(username, domainId) >> userAccount
        userAccount.getId() >> 1
        ldapManager.getDomainLinkedToLdap(domainId) >> new LdapTrustMapVO(domainId, type, name, (short)2)
        ldapManager.getUser(username, type.toString(), name) >> new LdapUser(username, "email", "firstname", "lastname", "principal", "domain", true, null)
        //user should be disabled in cloudstack
        accountManager.disableUser(1) >> userAccount

        when:
            Pair<Boolean, UserAuthenticator.ActionOnFailedAuthentication> result = ldapAuthenticator.authenticate(username, "password", domainId, null)
        then:
            result.first() == false
            result.second() == UserAuthenticator.ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT
    }

    def "test authentication when domain is linked and first time user can authenticate in ldap"(){
        LdapManager ldapManager = Mock(LdapManager)
        UserAccountDao userAccountDao = Mock(UserAccountDao)
        AccountManager accountManager = Mock(AccountManager)

        def ldapAuthenticator = new LdapAuthenticator()
        ldapAuthenticator._ldapManager = ldapManager
        ldapAuthenticator._userAccountDao = userAccountDao
        ldapAuthenticator._accountManager = accountManager

        long domainId = 1;
        String username = "rajanik"
        LdapManager.LinkType type = LdapManager.LinkType.GROUP
        String name = "CN=test,DC=ccp,DC=citrix,DC=com"

        ldapManager.isLdapEnabled() >> true
        userAccountDao.getUserAccount(username, domainId) >> null
        ldapManager.getDomainLinkedToLdap(domainId) >> new LdapTrustMapVO(domainId, type, name, (short)0)
        ldapManager.getUser(username, type.toString(), name) >> new LdapUser(username, "email", "firstname", "lastname", "principal", "domain", false, null)
        ldapManager.canAuthenticate(_, _, _) >> true
        //user should be created in cloudstack
        accountManager.createUserAccount(username, "", "firstname", "lastname", "email", null, username, (short) 2, domainId, username, null, _, _, User.Source.LDAP) >> Mock(UserAccount)

        when:
            Pair<Boolean, UserAuthenticator.ActionOnFailedAuthentication> result = ldapAuthenticator.authenticate(username, "password", domainId, null)
        then:
            result.first() == true
            result.second() == null
    }

    def "test authentication when domain is linked and existing user can authenticate in ldap"(){
        LdapManager ldapManager = Mock(LdapManager)
        UserAccountDao userAccountDao = Mock(UserAccountDao)
        AccountManager accountManager = Mock(AccountManager)

        def ldapAuthenticator = new LdapAuthenticator()
        ldapAuthenticator._ldapManager = ldapManager
        ldapAuthenticator._userAccountDao = userAccountDao
        ldapAuthenticator._accountManager = accountManager

        long domainId = 1;
        String username = "rajanik"
        LdapManager.LinkType type = LdapManager.LinkType.GROUP
        String name = "CN=test,DC=ccp,DC=citrix,DC=com"

        ldapManager.isLdapEnabled() >> true
        UserAccount userAccount = Mock(UserAccount)
        userAccountDao.getUserAccount(username, domainId) >> userAccount
        userAccount.getId() >> 1
        userAccount.getState() >> Account.State.DISABLED.toString()
        ldapManager.getDomainLinkedToLdap(domainId) >> new LdapTrustMapVO(domainId, type, name, (short)2)
        ldapManager.getUser(username, type.toString(), name) >> new LdapUser(username, "email", "firstname", "lastname", "principal", "domain", false, null)
        ldapManager.canAuthenticate(_, _, _) >> true
        //user should be enabled in cloudstack if disabled
        accountManager.enableUser(1) >> userAccount

        when:
        Pair<Boolean, UserAuthenticator.ActionOnFailedAuthentication> result = ldapAuthenticator.authenticate(username, "password", domainId, null)
        then:
        result.first() == true
        result.second() == null
    }

    def "test authentication when domain is linked and user cannot authenticate in ldap"(){
        LdapManager ldapManager = Mock(LdapManager)
        UserAccountDao userAccountDao = Mock(UserAccountDao)
        AccountManager accountManager = Mock(AccountManager)

        def ldapAuthenticator = new LdapAuthenticator()
        ldapAuthenticator._ldapManager = ldapManager
        ldapAuthenticator._userAccountDao = userAccountDao
        ldapAuthenticator._accountManager = accountManager

        long domainId = 1;
        String username = "rajanik"
        LdapManager.LinkType type = LdapManager.LinkType.GROUP
        String name = "CN=test,DC=ccp,DC=citrix,DC=com"

        ldapManager.isLdapEnabled() >> true
        UserAccount userAccount = Mock(UserAccount)
        userAccountDao.getUserAccount(username, domainId) >> userAccount
        ldapManager.getDomainLinkedToLdap(domainId) >> new LdapTrustMapVO(domainId, type, name, (short)2)
        ldapManager.getUser(username, type.toString(), name) >> new LdapUser(username, "email", "firstname", "lastname", "principal", "domain", false, null)
        ldapManager.canAuthenticate(_, _, _) >> false

        when:
        Pair<Boolean, UserAuthenticator.ActionOnFailedAuthentication> result = ldapAuthenticator.authenticate(username, "password", domainId, null)
        then:
            result.first() == false
            result.second() == UserAuthenticator.ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT
    }
}
