<commit_before>/*
 *  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 org.apache.directory.shared.client.api.operations.bind;


import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.net.InetAddress;

import org.apache.directory.api.ldap.model.constants.SaslQoP;
import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms;
import org.apache.directory.api.ldap.model.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.message.BindRequest;
import org.apache.directory.api.ldap.model.message.BindRequestImpl;
import org.apache.directory.api.ldap.model.message.BindResponse;
import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.ldap.client.api.LdapConnection;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.directory.ldap.client.api.SaslCramMd5Request;
import org.apache.directory.ldap.client.api.SaslDigestMd5Request;
import org.apache.directory.ldap.client.api.SaslGssApiRequest;
import org.apache.directory.server.annotations.CreateKdcServer;
import org.apache.directory.server.annotations.CreateLdapServer;
import org.apache.directory.server.annotations.CreateTransport;
import org.apache.directory.server.annotations.SaslMechanism;
import org.apache.directory.server.core.annotations.ApplyLdifs;
import org.apache.directory.server.core.annotations.ContextEntry;
import org.apache.directory.server.core.annotations.CreateDS;
import org.apache.directory.server.core.annotations.CreateIndex;
import org.apache.directory.server.core.annotations.CreatePartition;
import org.apache.directory.server.core.integ.AbstractLdapTestUnit;
import org.apache.directory.server.core.integ.FrameworkRunner;
import org.apache.directory.server.core.kerberos.KeyDerivationInterceptor;
import org.apache.directory.server.kerberos.kdc.KerberosTestUtils;
import org.apache.directory.server.ldap.LdapServer;
import org.apache.directory.server.ldap.handlers.extended.StoredProcedureExtendedOperationHandler;
import org.apache.directory.server.ldap.handlers.sasl.cramMD5.CramMd5MechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.digestMD5.DigestMd5MechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.gssapi.GssapiMechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.ntlm.NtlmMechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.plain.PlainMechanismHandler;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;


/**
 * An {@link AbstractServerTest} testing SASL authentication.
 * 
 * @author <a href="mailto:<EMAIL>@directory.apache.org">Apache Directory Project</a>
 */
@RunWith(FrameworkRunner.class)
@ApplyLdifs(
    {
        // Entry # 1
        "dn: ou=users,dc=example,dc=com",
        "objectClass: organizationalUnit",
        "objectClass: top",
        "ou: users\n",

        // Entry # 2
        "dn: uid=hnelson,ou=users,dc=example,dc=com",
        "objectClass: inetOrgPerson",
        "objectClass: organizationalPerson",
        "objectClass: person",
        "objectClass: krb5principal",
        "objectClass: krb5kdcentry",
        "objectClass: top",
        "uid: hnelson",
        "userPassword: <PASSWORD>",
        "krb5PrincipalName: <EMAIL>",
        "krb5KeyVersionNumber: 0",
        "cn: <NAME>",
        "sn: Nelson",

        // krbtgt
        "dn: uid=krbtgt,ou=users,dc=example,dc=com",
        "objectClass: inetOrgPerson",
        "objectClass: organizationalPerson",
        "objectClass: person",
        "objectClass: krb5principal",
        "objectClass: krb5kdcentry",
        "objectClass: top",
        "uid: krbtgt",
        "userPassword: <PASSWORD>",
        "krb5PrincipalName: krbtgt/EXAMPLE.<EMAIL>@EXAMPLE.<EMAIL>",
        "krb5KeyVersionNumber: 0",
        "cn: KDC Service",
        "sn: Service",

        // ldap per host
        "dn: uid=ldap,ou=users,dc=example,dc=com",
        "objectClass: inetOrgPerson",
        "objectClass: organizationalPerson",
        "objectClass: person",
        "objectClass: krb5principal",
        "objectClass: krb5kdcentry",
        "objectClass: top",
        "uid: ldap",
        "userPassword: <PASSWORD>",
        "krb5PrincipalName: ldap/<EMAIL>",
        "krb5KeyVersionNumber: 0",
        "cn: LDAP Service",
        "sn: Service" })
@CreateDS(
    allowAnonAccess = false,
    name = "SaslBindIT-class",
    partitions =
        {
            @CreatePartition(
                name = "example",
                suffix = "dc=example,dc=com",
                contextEntry =
                @ContextEntry(
                    entryLdif =
                    "dn: dc=example,dc=com\n" +
                        "dc: example\n" +
                        "objectClass: top\n" +
                        "objectClass: domain\n\n"),
                indexes =
                    {
                        @CreateIndex(attribute = "objectClass"),
                        @CreateIndex(attribute = "dc"),
                        @CreateIndex(attribute = "ou")
                })
    },
    additionalInterceptors =
        { KeyDerivationInterceptor.class })
@CreateLdapServer(transports =
    {
        @CreateTransport(protocol = "LDAP")
},
    saslHost = "localhost",
    saslPrincipal = "ldap/localhost@EXAMPLE.<EMAIL>",
    saslMechanisms =
        {
            @SaslMechanism(name = SupportedSaslMechanisms.PLAIN, implClass = PlainMechanismHandler.class),
            @SaslMechanism(name = SupportedSaslMechanisms.CRAM_MD5, implClass = CramMd5MechanismHandler.class),
            @SaslMechanism(name = SupportedSaslMechanisms.DIGEST_MD5, implClass = DigestMd5MechanismHandler.class),
            @SaslMechanism(name = SupportedSaslMechanisms.GSSAPI, implClass = GssapiMechanismHandler.class),
            @SaslMechanism(name = SupportedSaslMechanisms.NTLM, implClass = NtlmMechanismHandler.class),
            @SaslMechanism(name = SupportedSaslMechanisms.GSS_SPNEGO, implClass = NtlmMechanismHandler.class)
    },
    extendedOpHandlers =
        {
            StoredProcedureExtendedOperationHandler.class
    },
    ntlmProvider = BogusNtlmProvider.class)
@CreateKdcServer(
    transports =
        {
            @CreateTransport(protocol = "UDP", port = 6088),
            @CreateTransport(protocol = "TCP", port = 6088)
    })
public class SaslBindIT extends AbstractLdapTestUnit
{
    LdapServer ldapServer;
    
    @Before
    public void init() throws Exception
    {
        ldapServer = getLdapServer();
        KerberosTestUtils.fixServicePrincipalName( "ldap/" + InetAddress.getLocalHost().getHostName() + "@EXAMPLE.COM",
            new Dn( "uid=ldap,ou=users,dc=example,dc=com" ), getLdapServer() );
    }


    /**
     * Tests to make sure the server properly returns the supportedSASLMechanisms.
     */
    @Test
    public void testSupportedSASLMechanisms() throws Exception
    {
        // We have to tell the server that it should accept anonymous
        // auth, because we are reading the rootDSE
        ldapServer.getDirectoryService().setAllowAnonymousAccess( true );

        // Point on rootDSE
        String hostName = InetAddress.getLoopbackAddress().getHostName();
        LdapConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();

        Entry rootDn = connection.lookup( "", "supportedSASLMechanisms" );
        Attribute supportedSASLMechanisms = rootDn.get( "supportedSASLMechanisms" );

        assertEquals( 6, supportedSASLMechanisms.size() );
        assertTrue( supportedSASLMechanisms.contains( SupportedSaslMechanisms.GSSAPI ) );
        assertTrue( supportedSASLMechanisms.contains( SupportedSaslMechanisms.DIGEST_MD5 ) );
        assertTrue( supportedSASLMechanisms.contains( SupportedSaslMechanisms.CRAM_MD5 ) );
        assertTrue( supportedSASLMechanisms.contains( SupportedSaslMechanisms.NTLM ) );
        assertTrue( supportedSASLMechanisms.contains( SupportedSaslMechanisms.PLAIN ) );
        assertTrue( supportedSASLMechanisms.contains( SupportedSaslMechanisms.GSS_SPNEGO ) );
        
        connection.close();
    }


    /**
     * Tests to make sure PLAIN-binds works
     */
    @Test
    public void testSaslBindPLAIN() throws Exception
    {
        String hostName = InetAddress.getLoopbackAddress().getHostName();
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();

        BindResponse resp = connection.bindSaslPlain( "hnelson", "secret" );
        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );

        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
        Entry entry = connection.lookup( userDn );
        assertEquals( "hnelson", entry.get( "uid" ).getString() );

        connection.close();

        // Try to bind with a wrong user
        resp = connection.bindSaslPlain( "hnelsom", "secret" );
        assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, resp.getLdapResult().getResultCode() );

        // Try to bind with a wrong password
        resp = connection.bindSaslPlain( "hnelson", "secres" );
        assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, resp.getLdapResult().getResultCode() );
        
        connection.close();
    }


    /**
     * Test a SASL bind with an empty mechanism
     */
    @Test
    @Ignore("Activate and fix when DIRAPI-36 (Provide a SaslBindRequest extending BindRequest that can be used in LdapConnection.bind(...) method) is solved")
    public void testSaslBindNoMech() throws Exception
    {
        String hostName = InetAddress.getLoopbackAddress().getHostName();
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();

        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
        BindRequest bindReq = new BindRequestImpl();
        bindReq.setCredentials( "secret" );
        bindReq.setDn( userDn );
        bindReq.setSaslMechanism( "" ); // invalid mechanism
        bindReq.setSimple( false );

        try
        {
            connection.bind( bindReq );
            fail();
        }
        catch ( LdapException le )
        {
            //expected
        }

        connection.close();
    }


    /**
     * Tests to make sure CRAM-MD5 binds below the RootDSE work.
     */
    @Test
    public void testSaslCramMd5Bind() throws Exception
    {
        String hostName = InetAddress.getLoopbackAddress().getHostName();
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();

        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslCramMd5Request request = new SaslCramMd5Request();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "secret" );

        BindResponse resp = connection.bind( request );
        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );

        Entry entry = connection.lookup( userDn );
        assertEquals( "hnelson", entry.get( "uid" ).getString() );

        connection.close();
    }


    /**
     * Tests to make sure CRAM-MD5 binds below the RootDSE fail if the password is bad.
     */
    @Test
    public void testSaslCramMd5BindBadPassword() throws Exception
    {
        String hostName = InetAddress.getLoopbackAddress().getHostName();
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();

        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslCramMd5Request request = new SaslCramMd5Request();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "badsecret" );

        BindResponse resp = connection.bind( request );
        assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, resp.getLdapResult().getResultCode() );
        connection.close();
    }


    /**
     * Tests to make sure DIGEST-MD5 binds below the RootDSE work.
     */
    @Test
    public void testSaslDigestMd5Bind() throws Exception
    {
        String hostName = InetAddress.getLoopbackAddress().getHostName();
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();
        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslDigestMd5Request request = new SaslDigestMd5Request();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "secret" );
        request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
        BindResponse resp = connection.bind( request );
        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );

        Entry entry = connection.lookup( userDn );
        assertEquals( "hnelson", entry.get( "uid" ).getString() );

        connection.close();
    }


    /**
     * Tests to make sure DIGEST-MD5 binds below the RootDSE work with
     * SASL Quality of Protection set to 'auth'.
     */
    @Test
    public void testSaslDigestMd5BindSaslQoPAuth() throws Exception
    {
        String hostName = InetAddress.getLoopbackAddress().getHostName();
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();
        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslDigestMd5Request request = new SaslDigestMd5Request();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "secret" );
        request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
        request.setQualityOfProtection( SaslQoP.AUTH );
        BindResponse resp = connection.bind( request );
        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );

        Entry entry = connection.lookup( userDn );
        assertEquals( "hnelson", entry.get( "uid" ).getString() );

        connection.close();
    }


    /**
     * Tests to make sure DIGEST-MD5 binds below the RootDSE work with
     * SASL Quality of Protection set to 'auth-int'.
     */
    @Test
    @Ignore
    public void testSaslDigestMd5BindSaslQoPAuthInt() throws Exception
    {
        String hostName = InetAddress.getLoopbackAddress().getHostName();
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();
        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslDigestMd5Request request = new SaslDigestMd5Request();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "secret" );
        request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
        request.setQualityOfProtection( SaslQoP.AUTH_INT );
        BindResponse resp = connection.bind( request );
        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );

        Entry entry = connection.lookup( userDn );
        assertEquals( "hnelson", entry.get( "uid" ).getString() );

        connection.close();
    }


    /**
     * Tests to make sure DIGEST-MD5 binds below the RootDSE work with
     * SASL Quality of Protection set to 'auth-conf'.
     */
    @Test
    @Ignore
    public void testSaslDigestMd5BindSaslQoPAuthConf() throws Exception
    {
        String hostName = InetAddress.getLoopbackAddress().getHostName();
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();
        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslDigestMd5Request request = new SaslDigestMd5Request();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "secret" );
        request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
        request.setQualityOfProtection( SaslQoP.AUTH_CONF );
        BindResponse resp = connection.bind( request );
        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );

        Entry entry = connection.lookup( userDn );
        assertEquals( "hnelson", entry.get( "uid" ).getString() );

        connection.close();
    }


    /**
     * Tests to make sure DIGEST-MD5 binds below the RootDSE fail if the realm is bad.
     */
    @Test
    public void testSaslDigestMd5BindBadRealm() throws Exception
    {
        String hostName = InetAddress.getLoopbackAddress().getHostName();
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();
        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslDigestMd5Request request = new SaslDigestMd5Request();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "secret" );
        request.setRealmName( "badrealm.com" );
        BindResponse resp = connection.bind( request );
        assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, resp.getLdapResult().getResultCode() );

        connection.close();
    }


    /**
     * Tests to make sure DIGEST-MD5 binds below the RootDSE fail if the password is bad.
     */
    @Test
    public void testSaslDigestMd5BindBadPassword() throws Exception
    {
        String hostName = InetAddress.getLoopbackAddress().getHostName();
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();
        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslDigestMd5Request request = new SaslDigestMd5Request();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "<PASSWORD>" );
        request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
        BindResponse resp = connection.bind( request );
        assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, resp.getLdapResult().getResultCode() );

        connection.close();
    }


    /**
     * Tests to make sure GSS-API binds below the RootDSE work.
     */
    @Test
    public void testSaslGssApiBind() throws Exception
    {
        String hostName = InetAddress.getLoopbackAddress().getHostName();
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();

        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        kdcServer.getConfig().setPaEncTimestampRequired( false );

        SaslGssApiRequest request = new SaslGssApiRequest();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "secret" );
        request.setRealmName( ldapServer.getSaslRealms().get( 0 ).toUpperCase() );
        request.setKdcHost( InetAddress.getLocalHost().getHostName() );
        request.setKdcPort( 6088 );
        BindResponse resp = connection.bind( request );
        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );

        Entry entry = connection.lookup( userDn );
        assertEquals( "hnelson", entry.get( "uid" ).getString() );

        connection.close();
    }


    /**
     * Tests to make sure GSS-API binds below the RootDSE fail if the realm is bad.
     */
    @Test
    public void testSaslGssApiBindBadRealm() throws Exception
    {
        String hostName = InetAddress.getLoopbackAddress().getHostName();
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();

        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslGssApiRequest request = new SaslGssApiRequest();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "secret" );
        request.setRealmName( "badrealm.com" );
        request.setKdcHost( InetAddress.getLocalHost().getHostName() );
        request.setKdcPort( 6088 );
        
        try
        {
            connection.bind( request );
        }
        catch ( Exception e )
        {
            assertTrue( e instanceof LdapException );
        }
        finally
        {
            connection.close();
        }
    }


    /**
     * Tests to make sure GSS-API binds below the RootDSE fail if the password is bad.
     */
    @Test
    public void testSaslGssApiBindBadPassword() throws Exception
    {
        String hostName = InetAddress.getLoopbackAddress().getHostName();
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();

        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslGssApiRequest request = new SaslGssApiRequest();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "<PASSWORD>" );
        request.setRealmName( ldapServer.getSaslRealms().get( 0 ).toUpperCase() );
        request.setKdcHost( InetAddress.getLocalHost().getHostName() );
        request.setKdcPort( 6088 );
        
        try
        {
            connection.bind( request );
        }
        catch ( Exception e )
        {
            assertTrue( e instanceof LdapException );
        }
        finally
        {
            connection.close();
        }
    }<commit_msg>Use loopback hostname instead of localhost<commit_after>/*
 *  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 org.apache.directory.shared.client.api.operations.bind;


import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import org.apache.directory.api.ldap.model.constants.SaslQoP;
import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms;
import org.apache.directory.api.ldap.model.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.message.BindRequest;
import org.apache.directory.api.ldap.model.message.BindRequestImpl;
import org.apache.directory.api.ldap.model.message.BindResponse;
import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.util.Network;
import org.apache.directory.ldap.client.api.LdapConnection;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.directory.ldap.client.api.SaslCramMd5Request;
import org.apache.directory.ldap.client.api.SaslDigestMd5Request;
import org.apache.directory.ldap.client.api.SaslGssApiRequest;
import org.apache.directory.server.annotations.CreateKdcServer;
import org.apache.directory.server.annotations.CreateLdapServer;
import org.apache.directory.server.annotations.CreateTransport;
import org.apache.directory.server.annotations.SaslMechanism;
import org.apache.directory.server.core.annotations.ApplyLdifs;
import org.apache.directory.server.core.annotations.ContextEntry;
import org.apache.directory.server.core.annotations.CreateDS;
import org.apache.directory.server.core.annotations.CreateIndex;
import org.apache.directory.server.core.annotations.CreatePartition;
import org.apache.directory.server.core.integ.AbstractLdapTestUnit;
import org.apache.directory.server.core.integ.FrameworkRunner;
import org.apache.directory.server.core.kerberos.KeyDerivationInterceptor;
import org.apache.directory.server.kerberos.kdc.KerberosTestUtils;
import org.apache.directory.server.ldap.LdapServer;
import org.apache.directory.server.ldap.handlers.extended.StoredProcedureExtendedOperationHandler;
import org.apache.directory.server.ldap.handlers.sasl.cramMD5.CramMd5MechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.digestMD5.DigestMd5MechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.gssapi.GssapiMechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.ntlm.NtlmMechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.plain.PlainMechanismHandler;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;


/**
 * An {@link AbstractServerTest} testing SASL authentication.
 * 
 * @author <a href="mailto:<EMAIL>">Apache Directory Project</a>
 */
@RunWith(FrameworkRunner.class)
@ApplyLdifs(
    {
        // Entry # 1
        "dn: ou=users,dc=example,dc=com",
        "objectClass: organizationalUnit",
        "objectClass: top",
        "ou: users\n",

        // Entry # 2
        "dn: uid=hnelson,ou=users,dc=example,dc=com",
        "objectClass: inetOrgPerson",
        "objectClass: organizationalPerson",
        "objectClass: person",
        "objectClass: krb5principal",
        "objectClass: krb5kdcentry",
        "objectClass: top",
        "uid: hnelson",
        "userPassword: <PASSWORD>",
        "krb5PrincipalName: <EMAIL>",
        "krb5KeyVersionNumber: 0",
        "cn: <NAME>",
        "sn: Nelson",

        // krbtgt
        "dn: uid=krbtgt,ou=users,dc=example,dc=com",
        "objectClass: inetOrgPerson",
        "objectClass: organizationalPerson",
        "objectClass: person",
        "objectClass: krb5principal",
        "objectClass: krb5kdcentry",
        "objectClass: top",
        "uid: krbtgt",
        "userPassword: <PASSWORD>",
        "krb5PrincipalName: krbtgt/EXAMPLE.<EMAIL>@EXAMPLE.<EMAIL>",
        "krb5KeyVersionNumber: 0",
        "cn: KDC Service",
        "sn: Service",

        // ldap per host
        "dn: uid=ldap,ou=users,dc=example,dc=com",
        "objectClass: inetOrgPerson",
        "objectClass: organizationalPerson",
        "objectClass: person",
        "objectClass: krb5principal",
        "objectClass: krb5kdcentry",
        "objectClass: top",
        "uid: ldap",
        "userPassword: <PASSWORD>",
        "krb5PrincipalName: ldap/<EMAIL>",
        "krb5KeyVersionNumber: 0",
        "cn: LDAP Service",
        "sn: Service" })
@CreateDS(
    allowAnonAccess = false,
    name = "SaslBindIT-class",
    partitions =
        {
            @CreatePartition(
                name = "example",
                suffix = "dc=example,dc=com",
                contextEntry =
                @ContextEntry(
                    entryLdif =
                    "dn: dc=example,dc=com\n" +
                        "dc: example\n" +
                        "objectClass: top\n" +
                        "objectClass: domain\n\n"),
                indexes =
                    {
                        @CreateIndex(attribute = "objectClass"),
                        @CreateIndex(attribute = "dc"),
                        @CreateIndex(attribute = "ou")
                })
    },
    additionalInterceptors =
        { KeyDerivationInterceptor.class })
@CreateLdapServer(transports =
    {
        @CreateTransport(protocol = "LDAP")
},
    saslHost = "localhost",
    saslPrincipal = "ldap/<EMAIL>",
    saslMechanisms =
        {
            @SaslMechanism(name = SupportedSaslMechanisms.PLAIN, implClass = PlainMechanismHandler.class),
            @SaslMechanism(name = SupportedSaslMechanisms.CRAM_MD5, implClass = CramMd5MechanismHandler.class),
            @SaslMechanism(name = SupportedSaslMechanisms.DIGEST_MD5, implClass = DigestMd5MechanismHandler.class),
            @SaslMechanism(name = SupportedSaslMechanisms.GSSAPI, implClass = GssapiMechanismHandler.class),
            @SaslMechanism(name = SupportedSaslMechanisms.NTLM, implClass = NtlmMechanismHandler.class),
            @SaslMechanism(name = SupportedSaslMechanisms.GSS_SPNEGO, implClass = NtlmMechanismHandler.class)
    },
    extendedOpHandlers =
        {
            StoredProcedureExtendedOperationHandler.class
    },
    ntlmProvider = BogusNtlmProvider.class)
@CreateKdcServer(
    transports =
        {
            @CreateTransport(protocol = "UDP", port = 6088),
            @CreateTransport(protocol = "TCP", port = 6088)
    })
public class SaslBindIT extends AbstractLdapTestUnit
{
    LdapServer ldapServer;
    
    @Before
    public void init() throws Exception
    {
        ldapServer = getLdapServer();
        KerberosTestUtils.fixServicePrincipalName( "ldap/" + Network.LOOPBACK_HOSTNAME + "@EXAMPLE.COM",
            new Dn( "uid=ldap,ou=users,dc=example,dc=com" ), getLdapServer() );
    }


    /**
     * Tests to make sure the server properly returns the supportedSASLMechanisms.
     */
    @Test
    public void testSupportedSASLMechanisms() throws Exception
    {
        // We have to tell the server that it should accept anonymous
        // auth, because we are reading the rootDSE
        ldapServer.getDirectoryService().setAllowAnonymousAccess( true );

        // Point on rootDSE
        String hostName = Network.LOOPBACK_HOSTNAME;
        LdapConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();

        Entry rootDn = connection.lookup( "", "supportedSASLMechanisms" );
        Attribute supportedSASLMechanisms = rootDn.get( "supportedSASLMechanisms" );

        assertEquals( 6, supportedSASLMechanisms.size() );
        assertTrue( supportedSASLMechanisms.contains( SupportedSaslMechanisms.GSSAPI ) );
        assertTrue( supportedSASLMechanisms.contains( SupportedSaslMechanisms.DIGEST_MD5 ) );
        assertTrue( supportedSASLMechanisms.contains( SupportedSaslMechanisms.CRAM_MD5 ) );
        assertTrue( supportedSASLMechanisms.contains( SupportedSaslMechanisms.NTLM ) );
        assertTrue( supportedSASLMechanisms.contains( SupportedSaslMechanisms.PLAIN ) );
        assertTrue( supportedSASLMechanisms.contains( SupportedSaslMechanisms.GSS_SPNEGO ) );
        
        connection.close();
    }


    /**
     * Tests to make sure PLAIN-binds works
     */
    @Test
    public void testSaslBindPLAIN() throws Exception
    {
        String hostName = Network.LOOPBACK_HOSTNAME;
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();

        BindResponse resp = connection.bindSaslPlain( "hnelson", "secret" );
        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );

        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
        Entry entry = connection.lookup( userDn );
        assertEquals( "hnelson", entry.get( "uid" ).getString() );

        connection.close();

        // Try to bind with a wrong user
        resp = connection.bindSaslPlain( "hnelsom", "secret" );
        assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, resp.getLdapResult().getResultCode() );

        // Try to bind with a wrong password
        resp = connection.bindSaslPlain( "hnelson", "<PASSWORD>res" );
        assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, resp.getLdapResult().getResultCode() );
        
        connection.close();
    }


    /**
     * Test a SASL bind with an empty mechanism
     */
    @Test
    @Ignore("Activate and fix when DIRAPI-36 (Provide a SaslBindRequest extending BindRequest that can be used in LdapConnection.bind(...) method) is solved")
    public void testSaslBindNoMech() throws Exception
    {
        String hostName = Network.LOOPBACK_HOSTNAME;
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();

        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
        BindRequest bindReq = new BindRequestImpl();
        bindReq.setCredentials( "secret" );
        bindReq.setDn( userDn );
        bindReq.setSaslMechanism( "" ); // invalid mechanism
        bindReq.setSimple( false );

        try
        {
            connection.bind( bindReq );
            fail();
        }
        catch ( LdapException le )
        {
            //expected
        }

        connection.close();
    }


    /**
     * Tests to make sure CRAM-MD5 binds below the RootDSE work.
     */
    @Test
    public void testSaslCramMd5Bind() throws Exception
    {
        String hostName = Network.LOOPBACK_HOSTNAME;
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();

        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslCramMd5Request request = new SaslCramMd5Request();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "secret" );

        BindResponse resp = connection.bind( request );
        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );

        Entry entry = connection.lookup( userDn );
        assertEquals( "hnelson", entry.get( "uid" ).getString() );

        connection.close();
    }


    /**
     * Tests to make sure CRAM-MD5 binds below the RootDSE fail if the password is bad.
     */
    @Test
    public void testSaslCramMd5BindBadPassword() throws Exception
    {
        String hostName = Network.LOOPBACK_HOSTNAME;
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();

        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslCramMd5Request request = new SaslCramMd5Request();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "badsecret" );

        BindResponse resp = connection.bind( request );
        assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, resp.getLdapResult().getResultCode() );
        connection.close();
    }


    /**
     * Tests to make sure DIGEST-MD5 binds below the RootDSE work.
     */
    @Test
    public void testSaslDigestMd5Bind() throws Exception
    {
        String hostName = Network.LOOPBACK_HOSTNAME;
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();
        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslDigestMd5Request request = new SaslDigestMd5Request();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "secret" );
        request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
        BindResponse resp = connection.bind( request );
        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );

        Entry entry = connection.lookup( userDn );
        assertEquals( "hnelson", entry.get( "uid" ).getString() );

        connection.close();
    }


    /**
     * Tests to make sure DIGEST-MD5 binds below the RootDSE work with
     * SASL Quality of Protection set to 'auth'.
     */
    @Test
    public void testSaslDigestMd5BindSaslQoPAuth() throws Exception
    {
        String hostName = Network.LOOPBACK_HOSTNAME;
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();
        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslDigestMd5Request request = new SaslDigestMd5Request();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "secret" );
        request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
        request.setQualityOfProtection( SaslQoP.AUTH );
        BindResponse resp = connection.bind( request );
        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );

        Entry entry = connection.lookup( userDn );
        assertEquals( "hnelson", entry.get( "uid" ).getString() );

        connection.close();
    }


    /**
     * Tests to make sure DIGEST-MD5 binds below the RootDSE work with
     * SASL Quality of Protection set to 'auth-int'.
     */
    @Test
    @Ignore
    public void testSaslDigestMd5BindSaslQoPAuthInt() throws Exception
    {
        String hostName = Network.LOOPBACK_HOSTNAME;
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();
        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslDigestMd5Request request = new SaslDigestMd5Request();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "secret" );
        request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
        request.setQualityOfProtection( SaslQoP.AUTH_INT );
        BindResponse resp = connection.bind( request );
        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );

        Entry entry = connection.lookup( userDn );
        assertEquals( "hnelson", entry.get( "uid" ).getString() );

        connection.close();
    }


    /**
     * Tests to make sure DIGEST-MD5 binds below the RootDSE work with
     * SASL Quality of Protection set to 'auth-conf'.
     */
    @Test
    @Ignore
    public void testSaslDigestMd5BindSaslQoPAuthConf() throws Exception
    {
        String hostName = Network.LOOPBACK_HOSTNAME;
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();
        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslDigestMd5Request request = new SaslDigestMd5Request();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "secret" );
        request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
        request.setQualityOfProtection( SaslQoP.AUTH_CONF );
        BindResponse resp = connection.bind( request );
        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );

        Entry entry = connection.lookup( userDn );
        assertEquals( "hnelson", entry.get( "uid" ).getString() );

        connection.close();
    }


    /**
     * Tests to make sure DIGEST-MD5 binds below the RootDSE fail if the realm is bad.
     */
    @Test
    public void testSaslDigestMd5BindBadRealm() throws Exception
    {
        String hostName = Network.LOOPBACK_HOSTNAME;
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();
        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslDigestMd5Request request = new SaslDigestMd5Request();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "secret" );
        request.setRealmName( "badrealm.com" );
        BindResponse resp = connection.bind( request );
        assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, resp.getLdapResult().getResultCode() );

        connection.close();
    }


    /**
     * Tests to make sure DIGEST-MD5 binds below the RootDSE fail if the password is bad.
     */
    @Test
    public void testSaslDigestMd5BindBadPassword() throws Exception
    {
        String hostName = Network.LOOPBACK_HOSTNAME;
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();
        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslDigestMd5Request request = new SaslDigestMd5Request();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "badsecret" );
        request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
        BindResponse resp = connection.bind( request );
        assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, resp.getLdapResult().getResultCode() );

        connection.close();
    }


    /**
     * Tests to make sure GSS-API binds below the RootDSE work.
     */
    @Test
    public void testSaslGssApiBind() throws Exception
    {
        String hostName = Network.LOOPBACK_HOSTNAME;
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();

        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        kdcServer.getConfig().setPaEncTimestampRequired( false );

        SaslGssApiRequest request = new SaslGssApiRequest();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "secret" );
        request.setRealmName( ldapServer.getSaslRealms().get( 0 ).toUpperCase() );
        request.setKdcHost( Network.LOOPBACK_HOSTNAME );
        request.setKdcPort( 6088 );
        BindResponse resp = connection.bind( request );
        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );

        Entry entry = connection.lookup( userDn );
        assertEquals( "hnelson", entry.get( "uid" ).getString() );

        connection.close();
    }


    /**
     * Tests to make sure GSS-API binds below the RootDSE fail if the realm is bad.
     */
    @Test
    public void testSaslGssApiBindBadRealm() throws Exception
    {
        String hostName = Network.LOOPBACK_HOSTNAME;
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();

        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslGssApiRequest request = new SaslGssApiRequest();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "secret" );
        request.setRealmName( "badrealm.com" );
        request.setKdcHost( Network.LOOPBACK_HOSTNAME );
        request.setKdcPort( 6088 );
        
        try
        {
            connection.bind( request );
        }
        catch ( Exception e )
        {
            assertTrue( e instanceof LdapException );
        }
        finally
        {
            connection.close();
        }
    }


    /**
     * Tests to make sure GSS-API binds below the RootDSE fail if the password is bad.
     */
    @Test
    public void testSaslGssApiBindBadPassword() throws Exception
    {
        String hostName = Network.LOOPBACK_HOSTNAME;
        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
        connection.connect();

        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );

        SaslGssApiRequest request = new SaslGssApiRequest();
        request.setUsername( userDn.getRdn().getValue() );
        request.setCredentials( "badsecret" );
        request.setRealmName( ldapServer.getSaslRealms().get( 0 ).toUpperCase() );
        request.setKdcHost( Network.LOOPBACK_HOSTNAME );
        request.setKdcPort( 6088 );
        
        try
        {
            connection.bind( request );
        }
        catch ( Exception e )
        {
            assertTrue( e instanceof LdapException );
        }
        finally
        {
            connection.close();
        }
    }
{'fraction_non_alphanumeric': 0.10742708543502141, 'fraction_numerical': 0.0042594211670814, 'mean_word_length': 2.947261304309353, 'pattern_counts': {'":': 0, '<': 30, '<?xml version=': 0, '>': 30, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '23244', 'n_tokens_mistral': 13544, 'n_tokens_neox': 13335, 'n_words': 2631}