/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License;
 * you may not use this file except in compliance with the Elastic License.
 */

package org.elasticsearch.xpack.security.authc.pki;

import org.elasticsearch.ElasticsearchStatusException;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.ValidationException;
import org.elasticsearch.client.security.AuthenticateResponse;
import org.elasticsearch.client.security.PutRoleMappingRequest;
import org.elasticsearch.client.security.RefreshPolicy;
import org.elasticsearch.client.security.AuthenticateResponse.RealmInfo;
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
import org.elasticsearch.client.security.support.expressiondsl.fields.FieldRoleMapperExpression;
import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest;
import org.elasticsearch.client.security.DelegatePkiAuthenticationResponse;
import org.elasticsearch.client.security.InvalidateTokenRequest;
import org.elasticsearch.client.security.InvalidateTokenResponse;
import org.elasticsearch.client.security.user.User;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.test.SecurityIntegTestCase;
import org.elasticsearch.test.SecuritySettingsSourceField;
import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheRequestBuilder;
import org.elasticsearch.xpack.core.security.authc.support.Hasher;
import org.junit.Before;
import org.elasticsearch.test.SecuritySettingsSource;

import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Arrays;

import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.emptyCollectionOf;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.startsWith;

public class PkiAuthDelegationIntegTests extends SecurityIntegTestCase {

    @Override
    public Settings nodeSettings(int nodeOrdinal) {
        return Settings.builder()
                .put(super.nodeSettings(nodeOrdinal))
                .put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), true)
                // pki1 does not allow delegation
                .put("xpack.security.authc.realms.pki.pki1.order", "1")
                .putList("xpack.security.authc.realms.pki.pki1.certificate_authorities",
                    getDataPath("/org/elasticsearch/xpack/security/action/pki_delegation/testRootCA.crt").toString())
                .put("xpack.security.authc.realms.pki.pki1.files.role_mapping", getDataPath("role_mapping.yml"))
                // pki2 allows delegation but has a non-matching username pattern
                .put("xpack.security.authc.realms.pki.pki2.order", "2")
                .putList("xpack.security.authc.realms.pki.pki2.certificate_authorities",
                    getDataPath("/org/elasticsearch/xpack/security/action/pki_delegation/testRootCA.crt").toString())
                .put("xpack.security.authc.realms.pki.pki2.username_pattern", "CN=MISMATCH(.*?)(?:,|$)")
                .put("xpack.security.authc.realms.pki.pki2.delegation.enabled", true)
                .put("xpack.security.authc.realms.pki.pki2.files.role_mapping", getDataPath("role_mapping.yml"))
                // pki3 allows delegation and the username pattern (default) matches
                .put("xpack.security.authc.realms.pki.pki3.order", "3")
                .putList("xpack.security.authc.realms.pki.pki3.certificate_authorities",
                    getDataPath("/org/elasticsearch/xpack/security/action/pki_delegation/testRootCA.crt").toString())
                .put("xpack.security.authc.realms.pki.pki3.delegation.enabled", true)
                .put("xpack.security.authc.realms.pki.pki3.files.role_mapping", getDataPath("role_mapping.yml"))
                .build();
    }

    @Override
    protected String configUsers() {
        final String usersPasswdHashed = new String(Hasher.resolve(
            randomFrom("pbkdf2", "pbkdf2_1000", "bcrypt", "bcrypt9")).hash(SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING));
        return super.configUsers() +
            "user_manage:" + usersPasswdHashed + "\n" +
            "user_manage_security:" + usersPasswdHashed + "\n" +
            "user_delegate_pki:" + usersPasswdHashed + "\n" +
            "user_all:" + usersPasswdHashed + "\n" +
            "kibana_system:" + usersPasswdHashed + "\n";
    }

    @Override
    protected String configRoles() {
        return super.configRoles() + "\n" +
                "role_manage:\n" +
                "  cluster: [ manage ]\n" +
                "\n" +
                "role_manage_security:\n" +
                "  cluster: [ manage_security ]\n" +
                "\n" +
                "role_delegate_pki:\n" +
                "  cluster: [ delegate_pki ]\n" +
                "\n" +
                "role_all:\n" +
                "  cluster: [ all ]\n";
    }

    @Override
    protected String configUsersRoles() {
        return super.configUsersRoles() + "\n" +
                "role_manage:user_manage\n" +
                "role_manage_security:user_manage_security\n" +
                "role_delegate_pki:user_delegate_pki\n" +
                "role_all:user_all\n" +
                "kibana_system:kibana_system\n";
    }

    @Override
    protected boolean transportSSLEnabled() {
        return true;
    }

    @Override
    protected boolean addMockHttpTransport() {
        return false; // enable http
    }

    @Before
    void clearRealmCache() {
        new ClearRealmCacheRequestBuilder(client()).get();
    }

    public void testDelegateThenAuthenticate() throws Exception {
        final X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt");
        final X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt");
        final X509Certificate rootCA = readCertForPkiDelegation("testRootCA.crt");
        DelegatePkiAuthenticationRequest delegatePkiRequest;
        // trust root is optional
        if (randomBoolean()) {
            delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA));
        } else {
            delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA, rootCA));
        }

        try (RestHighLevelClient restClient = new TestRestHighLevelClient()) {
            for (String delegateeUsername : Arrays.asList("user_all", "user_delegate_pki", "kibana_system")) {
                // delegate
                RequestOptions.Builder optionsBuilder = RequestOptions.DEFAULT.toBuilder();
                optionsBuilder.addHeader("Authorization",
                        basicAuthHeaderValue(delegateeUsername, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING));
                DelegatePkiAuthenticationResponse delegatePkiResponse = restClient.security().delegatePkiAuthentication(delegatePkiRequest,
                        optionsBuilder.build());
                String token = delegatePkiResponse.getAccessToken();
                assertThat(token, is(notNullValue()));
                // authenticate
                optionsBuilder = RequestOptions.DEFAULT.toBuilder();
                optionsBuilder.addHeader("Authorization", "Bearer " + token);
                AuthenticateResponse resp = restClient.security().authenticate(optionsBuilder.build());
                User user = resp.getUser();
                assertThat(user, is(notNullValue()));
                assertThat(user.getUsername(), is("Elasticsearch Test Client"));
                RealmInfo authnRealm = resp.getAuthenticationRealm();
                assertThat(authnRealm, is(notNullValue()));
                assertThat(authnRealm.getName(), is("pki3"));
                assertThat(authnRealm.getType(), is("pki"));
            }
        }
    }

    public void testTokenInvalidate() throws Exception {
        final X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt");
        final X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt");
        final X509Certificate rootCA = readCertForPkiDelegation("testRootCA.crt");
        DelegatePkiAuthenticationRequest delegatePkiRequest;
        // trust root is optional
        if (randomBoolean()) {
            delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA));
        } else {
            delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA, rootCA));
        }

        try (RestHighLevelClient restClient = new TestRestHighLevelClient()) {
            String delegateeUsername = randomFrom("user_all", "user_delegate_pki", "kibana_system");
            // delegate
            RequestOptions.Builder optionsBuilder = RequestOptions.DEFAULT.toBuilder();
            optionsBuilder.addHeader("Authorization",
                    basicAuthHeaderValue(delegateeUsername, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING));
            DelegatePkiAuthenticationResponse delegatePkiResponse = restClient.security().delegatePkiAuthentication(delegatePkiRequest,
                    optionsBuilder.build());
            String token = delegatePkiResponse.getAccessToken();
            assertThat(token, is(notNullValue()));
            // authenticate
            optionsBuilder = RequestOptions.DEFAULT.toBuilder();
            optionsBuilder.addHeader("Authorization", "Bearer " + token);
            AuthenticateResponse resp = restClient.security().authenticate(optionsBuilder.build());
            User user = resp.getUser();
            assertThat(user, is(notNullValue()));
            assertThat(user.getUsername(), is("Elasticsearch Test Client"));
            assertThat(user.getMetadata().get("pki_dn"), is(notNullValue()));
            assertThat(user.getMetadata().get("pki_dn"), is("O=org, OU=Elasticsearch, CN=Elasticsearch Test Client"));
            assertThat(user.getMetadata().get("pki_delegated_by_user"), is(notNullValue()));
            assertThat(user.getMetadata().get("pki_delegated_by_user"), is(delegateeUsername));
            assertThat(user.getMetadata().get("pki_delegated_by_realm"), is(notNullValue()));
            assertThat(user.getMetadata().get("pki_delegated_by_realm"), is("file"));
            // no roles because no role mappings
            assertThat(user.getRoles(), is(emptyCollectionOf(String.class)));
            RealmInfo authnRealm = resp.getAuthenticationRealm();
            assertThat(authnRealm, is(notNullValue()));
            assertThat(authnRealm.getName(), is("pki3"));
            assertThat(authnRealm.getType(), is("pki"));
            // invalidate
            InvalidateTokenRequest invalidateRequest = new InvalidateTokenRequest(token, null, null, null);
            optionsBuilder = RequestOptions.DEFAULT.toBuilder();
            optionsBuilder.addHeader("Authorization",
                    basicAuthHeaderValue(delegateeUsername, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING));
            InvalidateTokenResponse invalidateResponse = restClient.security().invalidateToken(invalidateRequest, optionsBuilder.build());
            assertThat(invalidateResponse.getInvalidatedTokens(), is(1));
            assertThat(invalidateResponse.getErrorsCount(), is(0));
            // failed authenticate
            ElasticsearchStatusException e1 = expectThrows(ElasticsearchStatusException.class, () -> restClient.security()
                    .authenticate(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + token).build()));
            assertThat(e1.getMessage(), is("Elasticsearch exception [type=security_exception, reason=token expired]"));
        }
    }

    public void testDelegateUnauthorized() throws Exception {
        final X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt");
        final X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt");
        final X509Certificate rootCA = readCertForPkiDelegation("testRootCA.crt");
        DelegatePkiAuthenticationRequest delegatePkiRequest;
        // trust root is optional
        if (randomBoolean()) {
            delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA));
        } else {
            delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA, rootCA));
        }
        try (RestHighLevelClient restClient = new TestRestHighLevelClient()) {
            for (String delegateeUsername : Arrays.asList("user_manage", "user_manage_security")) {
                RequestOptions.Builder optionsBuilder = RequestOptions.DEFAULT.toBuilder();
                optionsBuilder.addHeader("Authorization",
                        basicAuthHeaderValue(delegateeUsername, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING));
                ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> {
                    restClient.security().delegatePkiAuthentication(delegatePkiRequest, optionsBuilder.build());
                });
                assertThat(e.getMessage(), startsWith("Elasticsearch exception [type=security_exception, reason=action"
                        + " [cluster:admin/xpack/security/delegate_pki] is unauthorized for user"));
            }
        }
    }

    public void testDelegatePkiWithRoleMapping() throws Exception {
        X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt");
        X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt");
        X509Certificate rootCA = readCertForPkiDelegation("testRootCA.crt");
        DelegatePkiAuthenticationRequest delegatePkiRequest;
        // trust root is optional
        if (randomBoolean()) {
            delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA));
        } else {
            delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA, rootCA));
        }
        final RequestOptions testUserOptions = RequestOptions.DEFAULT.toBuilder()
                .addHeader("Authorization", basicAuthHeaderValue(SecuritySettingsSource.TEST_USER_NAME,
                        new SecureString(SecuritySettingsSourceField.TEST_PASSWORD.toCharArray())))
                .build();
        try (RestHighLevelClient restClient = new TestRestHighLevelClient()) {
            // put role mappings for delegated PKI
            PutRoleMappingRequest request = new PutRoleMappingRequest("role_by_delegated_user", true,
                    Collections.singletonList("role_by_delegated_user"), Collections.emptyList(),
                    new FieldRoleMapperExpression("metadata.pki_delegated_by_user", "test_user"), null, RefreshPolicy.IMMEDIATE);
            restClient.security().putRoleMapping(request, testUserOptions);
            request = new PutRoleMappingRequest("role_by_delegated_realm", true, Collections.singletonList("role_by_delegated_realm"),
                    Collections.emptyList(), new FieldRoleMapperExpression("metadata.pki_delegated_by_realm", "file"), null,
                    RefreshPolicy.IMMEDIATE);
            restClient.security().putRoleMapping(request, testUserOptions);
            // delegate
            DelegatePkiAuthenticationResponse delegatePkiResponse = restClient.security().delegatePkiAuthentication(delegatePkiRequest,
                    testUserOptions);
            // authenticate
            AuthenticateResponse resp = restClient.security().authenticate(RequestOptions.DEFAULT.toBuilder()
                    .addHeader("Authorization", "Bearer " + delegatePkiResponse.getAccessToken()).build());
            User user = resp.getUser();
            assertThat(user, is(notNullValue()));
            assertThat(user.getUsername(), is("Elasticsearch Test Client"));
            assertThat(user.getMetadata().get("pki_dn"), is(notNullValue()));
            assertThat(user.getMetadata().get("pki_dn"), is("O=org, OU=Elasticsearch, CN=Elasticsearch Test Client"));
            assertThat(user.getMetadata().get("pki_delegated_by_user"), is(notNullValue()));
            assertThat(user.getMetadata().get("pki_delegated_by_user"), is("test_user"));
            assertThat(user.getMetadata().get("pki_delegated_by_realm"), is(notNullValue()));
            assertThat(user.getMetadata().get("pki_delegated_by_realm"), is("file"));
            // assert roles
            assertThat(user.getRoles(), containsInAnyOrder("role_by_delegated_user", "role_by_delegated_realm"));
            RealmInfo authnRealm = resp.getAuthenticationRealm();
            assertThat(authnRealm, is(notNullValue()));
            assertThat(authnRealm.getName(), is("pki3"));
            assertThat(authnRealm.getType(), is("pki"));
            // delete role mappings for delegated PKI
            restClient.security().deleteRoleMapping(new DeleteRoleMappingRequest("role_by_delegated_user", RefreshPolicy.IMMEDIATE),
                  testUserOptions);
            restClient.security().deleteRoleMapping(new DeleteRoleMappingRequest("role_by_delegated_realm", RefreshPolicy.IMMEDIATE),
                  testUserOptions);
        }
    }

    public void testIncorrectCertChain() throws Exception {
        X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt");
        X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt");
        X509Certificate bogusCertificate = readCertForPkiDelegation("bogus.crt");
        RequestOptions.Builder optionsBuilder = RequestOptions.DEFAULT.toBuilder();
        optionsBuilder.addHeader("Authorization", basicAuthHeaderValue(SecuritySettingsSource.TEST_USER_NAME,
                new SecureString(SecuritySettingsSourceField.TEST_PASSWORD.toCharArray())));
        try (RestHighLevelClient restClient = new TestRestHighLevelClient()) {
            // incomplete cert chain
            DelegatePkiAuthenticationRequest delegatePkiRequest1 = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate));
            ElasticsearchStatusException e1 = expectThrows(ElasticsearchStatusException.class,
                    () -> restClient.security().delegatePkiAuthentication(delegatePkiRequest1, optionsBuilder.build()));
            assertThat(e1.getMessage(), is("Elasticsearch exception [type=security_exception, reason=unable to authenticate user"
                    + " [O=org, OU=Elasticsearch, CN=Elasticsearch Test Client] for action [cluster:admin/xpack/security/delegate_pki]]"));
            // swapped order
            DelegatePkiAuthenticationRequest delegatePkiRequest2 = new DelegatePkiAuthenticationRequest(
                    Arrays.asList(intermediateCA, clientCertificate));
            ValidationException e2 = expectThrows(ValidationException.class,
                    () -> restClient.security().delegatePkiAuthentication(delegatePkiRequest2, optionsBuilder.build()));
            assertThat(e2.getMessage(), is("Validation Failed: 1: certificates chain must be an ordered chain;"));
            // bogus certificate
            DelegatePkiAuthenticationRequest delegatePkiRequest3 = new DelegatePkiAuthenticationRequest(Arrays.asList(bogusCertificate));
            ElasticsearchStatusException e3 = expectThrows(ElasticsearchStatusException.class,
                    () -> restClient.security().delegatePkiAuthentication(delegatePkiRequest3, optionsBuilder.build()));
            assertThat(e3.getMessage(), startsWith("Elasticsearch exception [type=security_exception, reason=unable to authenticate user"));
        }
    }

    private X509Certificate readCertForPkiDelegation(String certName) throws Exception {
        Path path = getDataPath("/org/elasticsearch/xpack/security/action/pki_delegation/" + certName);
        try (InputStream in = Files.newInputStream(path)) {
            CertificateFactory factory = CertificateFactory.getInstance("X.509");
            return (X509Certificate) factory.generateCertificate(in);
        }
    }

}
