diff --git a/http/servlet-undertow/pom.xml b/http/servlet-undertow/pom.xml index bdf9f165c..49398c31c 100644 --- a/http/servlet-undertow/pom.xml +++ b/http/servlet-undertow/pom.xml @@ -19,5 +19,9 @@ io.quarkus quarkus-micrometer + + io.quarkus + quarkus-security + diff --git a/http/servlet-undertow/src/main/java/io/quarkus/ts/http/undertow/security/ServletBasicAuthIdentityProvider.java b/http/servlet-undertow/src/main/java/io/quarkus/ts/http/undertow/security/ServletBasicAuthIdentityProvider.java new file mode 100644 index 000000000..9ab2fa2d2 --- /dev/null +++ b/http/servlet-undertow/src/main/java/io/quarkus/ts/http/undertow/security/ServletBasicAuthIdentityProvider.java @@ -0,0 +1,59 @@ +package io.quarkus.ts.http.undertow.security; + +import java.util.Set; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.control.ActivateRequestContext; + +import io.quarkus.arc.Arc; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.quarkus.security.runtime.QuarkusPrincipal; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; + +@ApplicationScoped +public class ServletBasicAuthIdentityProvider implements IdentityProvider { + @Override + public Class getRequestType() { + return UsernamePasswordAuthenticationRequest.class; + } + + @Override + public Uni authenticate(UsernamePasswordAuthenticationRequest usernamePasswordAuthenticationRequest, + AuthenticationRequestContext authenticationRequestContext) { + return authenticationRequestContext.runBlocking(() -> withIdentity(usernamePasswordAuthenticationRequest)); + } + + @ActivateRequestContext + SecurityIdentity withIdentity(UsernamePasswordAuthenticationRequest usernamePasswordAuthenticationRequest) { + final SecurityIdentity identity; + var username = usernamePasswordAuthenticationRequest.getUsername(); + var isPablo = "Pablo".equals(username); + if (isPablo || "Rocky".equals(username)) { + + if (!Arc.container().requestContext().isActive()) { + throw new IllegalStateException("The request scope should be active"); + } + + final Set roles; + if (isPablo) { + roles = Set.of("granados"); + } else { + // unauthorized + roles = Set.of(); + } + identity = QuarkusSecurityIdentity + .builder() + .setPrincipal(new QuarkusPrincipal(username)) + .addRoles(roles) + .build(); + } else { + // unauthenticated + identity = null; + } + return identity; + } +} diff --git a/http/servlet-undertow/src/main/java/io/quarkus/ts/http/undertow/servlets/SecuredWorld.java b/http/servlet-undertow/src/main/java/io/quarkus/ts/http/undertow/servlets/SecuredWorld.java new file mode 100644 index 000000000..b881e2ee9 --- /dev/null +++ b/http/servlet-undertow/src/main/java/io/quarkus/ts/http/undertow/servlets/SecuredWorld.java @@ -0,0 +1,40 @@ +package io.quarkus.ts.http.undertow.servlets; + +import java.io.IOException; +import java.io.PrintWriter; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.HttpConstraint; +import jakarta.servlet.annotation.ServletSecurity; +import jakarta.servlet.annotation.WebInitParam; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.jboss.logging.Logger; + +import io.vertx.ext.web.RoutingContext; + +@ServletSecurity(@HttpConstraint(rolesAllowed = { "pablo", "gonzalez", "granados" })) +@WebServlet(name = "SecuredWorldServlet", urlPatterns = "/secured", initParams = { + @WebInitParam(name = "message", value = "A secured message") }) +@ApplicationScoped +public class SecuredWorld extends HttpServlet { + + private static final Logger LOG = Logger.getLogger(SecuredWorld.class); + + @Inject + RoutingContext routingContext; + + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { + LOG.info(req.getSession().getId()); + PrintWriter writer = resp.getWriter(); + writer.write(routingContext.queryParam("secured-servlet-key").get(0)); + writer.close(); + } + +} diff --git a/http/servlet-undertow/src/main/resources/application.properties b/http/servlet-undertow/src/main/resources/application.properties index a46cb34e6..af20738f6 100644 --- a/http/servlet-undertow/src/main/resources/application.properties +++ b/http/servlet-undertow/src/main/resources/application.properties @@ -1,4 +1,6 @@ quarkus.http.root-path=/app quarkus.servlet.context-path=/servlet -quarkus.micrometer.export.json.enabled=true \ No newline at end of file +quarkus.micrometer.export.json.enabled=true + +quarkus.http.auth.basic=true diff --git a/http/servlet-undertow/src/test/java/io/quarkus/ts/http/undertow/HttpServletWithSessionListenerIT.java b/http/servlet-undertow/src/test/java/io/quarkus/ts/http/undertow/HttpServletWithSessionListenerIT.java index fc5a2fb95..e36219ff4 100644 --- a/http/servlet-undertow/src/test/java/io/quarkus/ts/http/undertow/HttpServletWithSessionListenerIT.java +++ b/http/servlet-undertow/src/test/java/io/quarkus/ts/http/undertow/HttpServletWithSessionListenerIT.java @@ -9,17 +9,25 @@ import org.apache.http.HttpStatus; import org.apache.http.util.Asserts; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; import io.quarkus.test.scenarios.QuarkusScenario; import io.restassured.RestAssured; +import io.restassured.response.ValidatableResponse; +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) @QuarkusScenario public class HttpServletWithSessionListenerIT { static final Duration ACTIVE_SESSION_TIMEOUT = Duration.ofMinutes(2); static final Duration REST_ASSURANCE_POLL_INTERVAL = Duration.ofSeconds(1); + @Order(1) @Test public void sessionEviction() { int activeSessions = 20; @@ -28,11 +36,39 @@ public void sessionEviction() { thenWaitToEvictSessionsAndCheckActiveSessionsEqualTo(0); } + @Tag("QUARKUS-2819") + @Order(2) + @Test + public void sessionSecured() { + // main objection is to test that CDI request scope can be activated during auth without + // having issue to use request scope later during processing + // session secured portion of this test is in order to stick to a test class theme + thenMakeSecuredWorldQuery("Rambo", 401); + thenCheckActiveSessionsEqualTo(0); + thenMakeSecuredWorldQuery("Rocky", 403); + thenCheckActiveSessionsEqualTo(0); + thenMakeSecuredWorldQuery("Pablo", 200).body(Matchers.is("secured-servlet-value")); + thenCheckActiveSessionsEqualTo(1); + } + private double getActiveSessions() { - return (Double) RestAssured.given().when() + var activeSessions = (Double) RestAssured.given().when() .get("/app/q/metrics") .then() .statusCode(HttpStatus.SC_OK).extract().as(Map.class).get(GAUGE_ACTIVE_SESSION); + if (activeSessions == null) { + return 0; + } + return activeSessions; + } + + private ValidatableResponse thenMakeSecuredWorldQuery(String user, int httpStatus) { + return RestAssured.given().when() + .auth().basic(user, user) + .queryParam("secured-servlet-key", "secured-servlet-value") + .get("/app/servlet/secured") + .then() + .statusCode(httpStatus); } private void thenMakeHelloWorldQuery(int requestAmount) {