//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.ee10.servlet.security;

import java.io.IOException;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashSet;
import java.util.Set;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.SessionHandler;
import org.eclipse.jetty.ee10.servlet.security.ConstraintMapping;
import org.eclipse.jetty.security.Constraint;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.security.Password;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * @version $Revision: 1441 $ $Date: 2010-04-02 12:28:17 +0200 (Fri, 02 Apr 2010) $
 */
public class SpecExampleConstraintTest
{
    private static final String TEST_REALM = "TestRealm";
    private Server _server;
    private LocalConnector _connector;
    private ConstraintSecurityHandler _security;

    @BeforeEach
    public void setupServer()
    {
        _server = new Server();
        _connector = new LocalConnector(_server);
        _server.addConnector(_connector);

        ServletContextHandler context = new ServletContextHandler();
        context.setContextPath("/ctx");

        TestLoginService loginService = new TestLoginService(TEST_REALM);

        loginService.putUser("fred", new Password("password"), new String[0]);
        loginService.putUser("harry", new Password("password"), new String[]{"HOMEOWNER"});
        loginService.putUser("chris", new Password("password"), new String[]{"CONTRACTOR"});
        loginService.putUser("steven", new Password("password"), new String[]{"SALESCLERK"});
        _server.addBean(loginService);
        
        context.addServlet(TestServlet.class, "/");
        _security = new ConstraintSecurityHandler();
        context.setSecurityHandler(_security);
        SessionHandler sessions = new SessionHandler();
        context.setSessionHandler(sessions);

        _server.setHandler(context);
        
        /*
        <security-constraint>
        <web-resource-collection>
        <web-resource-name>precluded methods</web-resource-name>
        <url-pattern>/*</url-pattern>
        <url-pattern>/acme/wholesale/*</url-pattern>
        <url-pattern>/acme/retail/*</url-pattern>
        <http-method-omission>GET</http-method-omission>
        <http-method-omission>POST</http-method-omission>
        </web-resource-collection>
        <auth-constraint/>
        </security-constraint>
        */

        Constraint constraint0 = new Constraint.Builder().authorization(Constraint.Authorization.FORBIDDEN)
            .name("precluded methods")
            .build();
        ConstraintMapping mapping0 = new ConstraintMapping();
        mapping0.setPathSpec("/*");
        mapping0.setConstraint(constraint0);
        mapping0.setMethodOmissions(new String[]{"GET", "POST"});

        ConstraintMapping mapping1 = new ConstraintMapping();
        mapping1.setPathSpec("/acme/wholesale/*");
        mapping1.setConstraint(constraint0);
        mapping1.setMethodOmissions(new String[]{"GET", "POST"});

        ConstraintMapping mapping2 = new ConstraintMapping();
        mapping2.setPathSpec("/acme/retail/*");
        mapping2.setConstraint(constraint0);
        mapping2.setMethodOmissions(new String[]{"GET", "POST"});
        
        /*
        <security-constraint>
        <web-resource-collection>
        <web-resource-name>wholesale</web-resource-name>
        <url-pattern>/acme/wholesale/*</url-pattern>
        <http-method>GET</http-method>
        <http-method>PUT</http-method>
        </web-resource-collection>
        <auth-constraint>
        <role-name>SALESCLERK</role-name>
        </auth-constraint>
        </security-constraint>
        */
        Constraint constraint1 = new Constraint.Builder()
            .name("wholesale")
            .roles("SALESCLEARK")
            .build();
        ConstraintMapping mapping3 = new ConstraintMapping();
        mapping3.setPathSpec("/acme/wholesale/*");
        mapping3.setConstraint(constraint1);
        mapping3.setMethod("GET");
        ConstraintMapping mapping4 = new ConstraintMapping();
        mapping4.setPathSpec("/acme/wholesale/*");
        mapping4.setConstraint(constraint1);
        mapping4.setMethod("PUT");

        /*
        <security-constraint>
          <web-resource-collection>
            <web-resource-name>wholesale 2</web-resource-name>
            <url-pattern>/acme/wholesale/*</url-pattern>
            <http-method>GET</http-method>
            <http-method>POST</http-method>
          </web-resource-collection>
          <auth-constraint>
            <role-name>CONTRACTOR</role-name>
          </auth-constraint>
          <user-data-constraint>
             <transport-guarantee>CONFIDENTIAL</transport-guarantee>
          </user-data-constraint>
        </security-constraint>
         */
        Constraint constraint2 = new Constraint.Builder()
            .name("wholesale 2")
            .roles("CONTRACTOR")
            .transport(Constraint.Transport.SECURE)
            .build();
        ConstraintMapping mapping5 = new ConstraintMapping();
        mapping5.setPathSpec("/acme/wholesale/*");
        mapping5.setMethod("GET");
        mapping5.setConstraint(constraint2);
        ConstraintMapping mapping6 = new ConstraintMapping();
        mapping6.setPathSpec("/acme/wholesale/*");
        mapping6.setMethod("POST");
        mapping6.setConstraint(constraint2);
        
        /*
          <security-constraint>
           <web-resource-collection>
            <web-resource-name>retail</web-resource-name>
            <url-pattern>/acme/retail/*</url-pattern>
            <http-method>GET</http-method>
            <http-method>POST</http-method>
           </web-resource-collection>
           <auth-constraint>
            <role-name>CONTRACTOR</role-name>
            <role-name>HOMEOWNER</role-name>
           </auth-constraint>
          </security-constraint>
         */
        Constraint constraint4 = new Constraint.Builder()
            .name("retail")
            .roles("CONTRACTOR", "HOMEOWNER")
            .build();
        ConstraintMapping mapping7 = new ConstraintMapping();
        mapping7.setPathSpec("/acme/retail/*");
        mapping7.setMethod("GET");
        mapping7.setConstraint(constraint4);
        ConstraintMapping mapping8 = new ConstraintMapping();
        mapping8.setPathSpec("/acme/retail/*");
        mapping8.setMethod("POST");
        mapping8.setConstraint(constraint4);

        Set<String> knownRoles = new HashSet<>();
        knownRoles.add("CONTRACTOR");
        knownRoles.add("HOMEOWNER");
        knownRoles.add("SALESCLERK");

        _security.setConstraintMappings(
            Arrays.asList(mapping0, mapping1, mapping2, mapping3, mapping4, mapping5, mapping6, mapping7, mapping8),
            knownRoles);
    }

    @AfterEach
    public void stopServer() throws Exception
    {
        if (_server.isRunning())
        {
            _server.stop();
            _server.join();
        }
    }

    @Test
    public void testUncoveredHttpMethodDetection() throws Exception
    {
        _security.setAuthenticator(new BasicAuthenticator());
        _server.start();

        Set<String> paths = _security.getPathsWithUncoveredHttpMethods();
        assertEquals(1, paths.size());
        assertEquals("/*", paths.iterator().next());
    }

    @Test
    public void testUncoveredHttpMethodsDenied() throws Exception
    {
        try
        {
            _security.setDenyUncoveredHttpMethods(false);
            _security.setAuthenticator(new BasicAuthenticator());
            _server.start();

            //There are uncovered methods for GET/POST at url /*
            //without deny-uncovered-http-methods they should be accessible
            String response;
            response = _connector.getResponse("GET /ctx/index.html HTTP/1.0\r\n\r\n");
            assertThat(response, startsWith("HTTP/1.1 200 OK"));

            //set deny-uncovered-http-methods true
            _security.setDenyUncoveredHttpMethods(true);

            //check they cannot be accessed
            response = _connector.getResponse("GET /ctx/index.html HTTP/1.0\r\n\r\n");
            assertTrue(response.startsWith("HTTP/1.1 403 Forbidden"));
        }
        finally
        {
            _security.setDenyUncoveredHttpMethods(false);
        }
    }

    @Test
    public void testNoAuth() throws Exception
    {
        _server.start();
        
        String response = _connector.getResponse("GET /ctx/foo HTTP/1.0\r\n\r\n");
        assertTrue(response.startsWith("HTTP/1.1 200 OK"));
    }
    
    @Test
    public void testBasic() throws Exception
    {
        _security.setAuthenticator(new BasicAuthenticator());
        _server.start();

        String response;
        /*
          /*                 all methods except GET/POST forbidden
          /acme/wholesale/*  all methods except GET/POST forbidden
          /acme/retail/*     all methods except GET/POST forbidden
          /acme/wholesale/*  GET must be in role CONTRACTOR or SALESCLERK
          /acme/wholesale/*  POST must be in role CONTRACTOR and confidential transport
          /acme/retail/*     GET must be in role CONTRACTOR or HOMEOWNER
          /acme/retail/*     POST must be in role CONTRACTOR or HOMEOWNER
        */

        //a user in role HOMEOWNER is forbidden HEAD request
        response = _connector.getResponse("HEAD /ctx/index.html HTTP/1.0\r\n\r\n");
        assertTrue(response.startsWith("HTTP/1.1 403 Forbidden"));

        Base64.Encoder authEncoder = Base64.getEncoder();
        String encodedHarry = authEncoder.encodeToString("harry:password".getBytes(ISO_8859_1));
        String encodedChris = authEncoder.encodeToString("chris:password".getBytes(ISO_8859_1));

        response = _connector.getResponse("HEAD /ctx/index.html HTTP/1.0\r\n" +
            "Authorization: Basic " + encodedHarry + "\r\n" +
            "\r\n");
        assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));

        response = _connector.getResponse("HEAD /ctx/acme/wholesale/index.html HTTP/1.0\r\n" +
            "Authorization: Basic " + encodedHarry + "\r\n" +
            "\r\n");
        assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));

        response = _connector.getResponse("HEAD /ctx/acme/retail/index.html HTTP/1.0\r\n" +
            "Authorization: Basic " + encodedHarry + "\r\n" +
            "\r\n");
        assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));

        //a user in role CONTRACTOR can do a GET
        response = _connector.getResponse("GET /ctx/acme/wholesale/index.html HTTP/1.0\r\n" +
            "Authorization: Basic " + encodedChris + "\r\n" +
            "\r\n");
        assertThat(response, startsWith("HTTP/1.1 200 OK"));

        //a user in role CONTRACTOR can only do a post if confidential
        response = _connector.getResponse("POST /ctx/acme/wholesale/index.html HTTP/1.0\r\n" +
            "Authorization: Basic " + encodedChris + "\r\n" +
            "\r\n");
        assertThat(response, startsWith("HTTP/1.1 403 Forbidden"));
        assertThat(response, containsString("!Secure"));

        //a user in role HOMEOWNER can do a GET
        response = _connector.getResponse("GET /ctx/acme/retail/index.html HTTP/1.0\r\n" +
            "Authorization: Basic " + encodedHarry + "\r\n" +
            "\r\n");
        assertThat(response, startsWith("HTTP/1.1 200 OK"));
    }

    public static class TestServlet extends HttpServlet
    {
        @Override
        protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
        {
            resp.setContentType("text/plain; charset=UTF-8");
            resp.getWriter().println("""
                URI=%s
                user=%s
                %s
                """.formatted(req.getRequestURI(), req.getRemoteUser(), req.getParameter("test_parameter")));
        }
    }
}
