/*
 * 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 com.datastax.dse.driver.api.core.auth;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;

import com.datastax.dse.driver.api.core.config.DseDriverOption;
import com.datastax.dse.driver.internal.core.auth.DseGssApiAuthProvider;
import com.datastax.oss.driver.api.core.AllNodesFailedException;
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.auth.AuthenticationException;
import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
import com.datastax.oss.driver.api.core.cql.ResultSet;
import com.datastax.oss.driver.api.core.cql.SimpleStatement;
import com.datastax.oss.driver.api.core.servererrors.UnauthorizedException;
import com.datastax.oss.driver.api.testinfra.ccm.SchemaChangeSynchronizer;
import com.datastax.oss.driver.api.testinfra.requirement.BackendRequirement;
import com.datastax.oss.driver.api.testinfra.requirement.BackendType;
import com.datastax.oss.driver.api.testinfra.session.SessionUtils;
import com.datastax.oss.driver.internal.core.auth.PlainTextAuthProvider;
import java.util.List;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;

@BackendRequirement(
    type = BackendType.DSE,
    minInclusive = "5.1",
    description = "Required for DseAuthenticator with proxy")
public class DseProxyAuthenticationIT {
  private static String bobPrincipal;
  private static String charliePrincipal;
  @ClassRule public static EmbeddedAdsRule ads = new EmbeddedAdsRule();

  @BeforeClass
  public static void addUsers() {
    bobPrincipal = ads.addUserAndCreateKeyTab("bob", "fakePasswordForBob");
    charliePrincipal = ads.addUserAndCreateKeyTab("charlie", "fakePasswordForCharlie");
  }

  @Before
  public void setupRoles() {

    SchemaChangeSynchronizer.withLock(
        () -> {
          try (CqlSession session = ads.newKeyTabSession()) {
            session.execute(
                "CREATE ROLE IF NOT EXISTS alice WITH PASSWORD = 'fakePasswordForAlice' AND LOGIN = FALSE");
            session.execute(
                "CREATE ROLE IF NOT EXISTS ben WITH PASSWORD = 'fakePasswordForBen' AND LOGIN = TRUE");
            session.execute("CREATE ROLE IF NOT EXISTS 'bob@DATASTAX.COM' WITH LOGIN = TRUE");
            session.execute(
                "CREATE ROLE IF NOT EXISTS 'charlie@DATASTAX.COM' WITH PASSWORD = 'fakePasswordForCharlie' AND LOGIN = TRUE");
            session.execute(
                "CREATE ROLE IF NOT EXISTS steve WITH PASSWORD = 'fakePasswordForSteve' AND LOGIN = TRUE");
            session.execute(
                "CREATE KEYSPACE IF NOT EXISTS aliceks WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':'1'}");
            session.execute(
                "CREATE TABLE IF NOT EXISTS aliceks.alicetable (key text PRIMARY KEY, value text)");
            session.execute(
                "INSERT INTO aliceks.alicetable (key, value) VALUES ('hello', 'world')");
            session.execute("GRANT ALL ON KEYSPACE aliceks TO alice");
            session.execute("GRANT EXECUTE ON ALL AUTHENTICATION SCHEMES TO 'ben'");
            session.execute("GRANT EXECUTE ON ALL AUTHENTICATION SCHEMES TO 'bob@DATASTAX.COM'");
            session.execute("GRANT EXECUTE ON ALL AUTHENTICATION SCHEMES TO 'steve'");
            session.execute(
                "GRANT EXECUTE ON ALL AUTHENTICATION SCHEMES TO 'charlie@DATASTAX.COM'");
            session.execute("GRANT PROXY.LOGIN ON ROLE 'alice' TO 'ben'");
            session.execute("GRANT PROXY.LOGIN ON ROLE 'alice' TO 'bob@DATASTAX.COM'");
            session.execute("GRANT PROXY.EXECUTE ON ROLE 'alice' TO 'steve'");
            session.execute("GRANT PROXY.EXECUTE ON ROLE 'alice' TO 'charlie@DATASTAX.COM'");
            // ben and bob are allowed to login as alice, but not execute as alice.
            // charlie and steve are allowed to execute as alice, but not login as alice.
          }
        });
  }
  /**
   * Validates that a connection may be successfully made as user 'alice' using the credentials of a
   * user 'ben' using {@link PlainTextAuthProvider} assuming ben has PROXY.LOGIN authorization on
   * alice.
   */
  @Test
  public void should_allow_plain_text_authorized_user_to_login_as() {
    try (CqlSession session =
        SessionUtils.newSession(
            ads.ccm,
            SessionUtils.configLoaderBuilder()
                .withString(DseDriverOption.AUTH_PROVIDER_AUTHORIZATION_ID, "alice")
                .withString(DefaultDriverOption.AUTH_PROVIDER_USER_NAME, "ben")
                .withString(DefaultDriverOption.AUTH_PROVIDER_PASSWORD, "fakePasswordForBen")
                .withClass(DefaultDriverOption.AUTH_PROVIDER_CLASS, PlainTextAuthProvider.class)
                .build())) {
      SimpleStatement select = SimpleStatement.builder("select * from aliceks.alicetable").build();
      ResultSet set = session.execute(select);
      assertThat(set).isNotNull();
    }
  }

  @Test
  public void should_allow_plain_text_authorized_user_to_login_as_programmatically() {
    try (CqlSession session =
        CqlSession.builder()
            .addContactEndPoints(ads.ccm.getContactPoints())
            .withAuthCredentials("ben", "fakePasswordForBen", "alice")
            .build()) {
      session.execute("select * from system.local");
    }
  }

  /**
   * Validates that a connection may successfully made as user 'alice' using the credentials of a
   * principal 'bob@DATASTAX.COM' using {@link DseGssApiAuthProvider} assuming 'bob@DATASTAX.COM'
   * has PROXY.LOGIN authorization on alice.
   */
  @Test
  public void should_allow_kerberos_authorized_user_to_login_as() {
    try (CqlSession session =
        ads.newKeyTabSession(
            bobPrincipal, ads.getKeytabForPrincipal(bobPrincipal).getAbsolutePath(), "alice")) {
      SimpleStatement select = SimpleStatement.builder("select * from aliceks.alicetable").build();
      ResultSet set = session.execute(select);
      assertThat(set).isNotNull();
    }
  }

  /**
   * Validates that a connection does not succeed as user 'alice' using the credentials of a user
   * 'steve' assuming 'steve' does not have PROXY.LOGIN authorization on alice.
   */
  @Test
  public void should_not_allow_plain_text_unauthorized_user_to_login_as() {
    try (CqlSession session =
        SessionUtils.newSession(
            ads.ccm,
            SessionUtils.configLoaderBuilder()
                .withString(DseDriverOption.AUTH_PROVIDER_AUTHORIZATION_ID, "alice")
                .withString(DefaultDriverOption.AUTH_PROVIDER_USER_NAME, "steve")
                .withString(DefaultDriverOption.AUTH_PROVIDER_PASSWORD, "fakePasswordForSteve")
                .withClass(DefaultDriverOption.AUTH_PROVIDER_CLASS, PlainTextAuthProvider.class)
                .build())) {
      SimpleStatement select = SimpleStatement.builder("select * from aliceks.alicetable").build();
      session.execute(select);
      fail("Should have thrown AllNodesFailedException on login.");
    } catch (AllNodesFailedException anfe) {
      verifyException(anfe);
    }
  }
  /**
   * Validates that a connection does not succeed as user 'alice' using the credentials of a
   * principal 'charlie@DATASTAX.COM' assuming 'charlie@DATASTAX.COM' does not have PROXY.LOGIN
   * authorization on alice.
   */
  @Test
  public void should_not_allow_kerberos_unauthorized_user_to_login_as() throws Exception {
    try (CqlSession session =
        ads.newKeyTabSession(
            charliePrincipal,
            ads.getKeytabForPrincipal(charliePrincipal).getAbsolutePath(),
            "alice")) {
      SimpleStatement select = SimpleStatement.builder("select * from aliceks.alicetable").build();
      session.execute(select);
      fail("Should have thrown AllNodesFailedException on login.");
    } catch (AllNodesFailedException anfe) {
      verifyException(anfe);
    }
  }
  /**
   * Validates that a query may be successfully made as user 'alice' using a {@link CqlSession} that
   * is authenticated to user 'steve' using {@link PlainTextAuthProvider} assuming steve has
   * PROXY.EXECUTE authorization on alice.
   */
  @Test
  public void should_allow_plain_text_authorized_user_to_execute_as() {
    try (CqlSession session =
        SessionUtils.newSession(
            ads.ccm,
            SessionUtils.configLoaderBuilder()
                .withString(DefaultDriverOption.AUTH_PROVIDER_USER_NAME, "steve")
                .withString(DefaultDriverOption.AUTH_PROVIDER_PASSWORD, "fakePasswordForSteve")
                .withClass(DefaultDriverOption.AUTH_PROVIDER_CLASS, PlainTextAuthProvider.class)
                .build())) {
      SimpleStatement select = SimpleStatement.builder("select * from aliceks.alicetable").build();
      SimpleStatement statementAsAlice = ProxyAuthentication.executeAs("alice", select);
      ResultSet set = session.execute(statementAsAlice);
      assertThat(set).isNotNull();
    }
  }
  /**
   * Validates that a query may be successfully made as user 'alice' using a {@link CqlSession} that
   * is authenticated to principal 'charlie@DATASTAX.COM' using {@link DseGssApiAuthProvider}
   * assuming charlie@DATASTAX.COM has PROXY.EXECUTE authorization on alice.
   */
  @Test
  public void should_allow_kerberos_authorized_user_to_execute_as() {
    try (CqlSession session =
        ads.newKeyTabSession(
            charliePrincipal, ads.getKeytabForPrincipal(charliePrincipal).getAbsolutePath())) {
      SimpleStatement select = SimpleStatement.builder("select * from aliceks.alicetable").build();
      SimpleStatement statementAsAlice = ProxyAuthentication.executeAs("alice", select);
      ResultSet set = session.execute(statementAsAlice);
      assertThat(set).isNotNull();
    }
  }
  /**
   * Validates that a query may not be made as user 'alice' using a {@link CqlSession} that is
   * authenticated to user 'ben' if ben does not have PROXY.EXECUTE authorization on alice.
   */
  @Test
  public void should_not_allow_plain_text_unauthorized_user_to_execute_as() {
    try (CqlSession session =
        SessionUtils.newSession(
            ads.ccm,
            SessionUtils.configLoaderBuilder()
                .withString(DefaultDriverOption.AUTH_PROVIDER_USER_NAME, "ben")
                .withString(DefaultDriverOption.AUTH_PROVIDER_PASSWORD, "fakePasswordForBen")
                .withClass(DefaultDriverOption.AUTH_PROVIDER_CLASS, PlainTextAuthProvider.class)
                .build())) {
      SimpleStatement select = SimpleStatement.builder("select * from aliceks.alicetable").build();
      SimpleStatement statementAsAlice = ProxyAuthentication.executeAs("alice", select);
      session.execute(statementAsAlice);
      fail("Should have thrown UnauthorizedException on executeAs.");
    } catch (UnauthorizedException ue) {
      verifyException(ue, "ben");
    }
  }
  /**
   * Validates that a query may not be made as user 'alice' using a {@link CqlSession} that is
   * authenticated to principal 'bob@DATASTAX.COM' using {@link DseGssApiAuthProvider} if
   * bob@DATASTAX.COM does not have PROXY.EXECUTE authorization on alice.
   */
  @Test
  public void should_not_allow_kerberos_unauthorized_user_to_execute_as() {
    try (CqlSession session =
        ads.newKeyTabSession(
            bobPrincipal, ads.getKeytabForPrincipal(bobPrincipal).getAbsolutePath())) {
      SimpleStatement select = SimpleStatement.builder("select * from aliceks.alicetable").build();
      SimpleStatement statementAsAlice = ProxyAuthentication.executeAs("alice", select);
      session.execute(statementAsAlice);
      fail("Should have thrown UnauthorizedException on executeAs.");
    } catch (UnauthorizedException ue) {
      verifyException(ue, "bob@DATASTAX.COM");
    }
  }

  private void verifyException(AllNodesFailedException anfe) {
    assertThat(anfe.getAllErrors()).hasSize(1);
    List<Throwable> errors = anfe.getAllErrors().values().iterator().next();
    assertThat(errors).hasSize(1);
    Throwable firstError = errors.get(0);
    assertThat(firstError)
        .isInstanceOf(AuthenticationException.class)
        .hasMessageContaining(
            "Authentication error on node /127.0.0.1:9042: "
                + "server replied with 'Failed to login. Please re-try.' to AuthResponse request");
  }

  private void verifyException(UnauthorizedException ue, String user) {
    assertThat(ue.getMessage())
        .contains(
            String.format(
                "Either '%s' does not have permission to execute queries as 'alice' "
                    + "or that role does not exist.",
                user));
  }
}
