Skip to content

Commit

Permalink
Merge pull request quarkusio#44554 from michalvavrik/feature/enable-m…
Browse files Browse the repository at this point in the history
…anagement-interface-auth-without-basic-auth

Allow to enable security for the Management interface without enabling basic authentication and document support for other mechanisms
  • Loading branch information
sberyozkin authored Nov 18, 2024
2 parents bd567a8 + 57bf2f8 commit 05b92aa
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 5 deletions.
32 changes: 32 additions & 0 deletions docs/src/main/asciidoc/management-interface-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,38 @@ Until https://github.com/knative/serving/issues/8471[KNative#8471] is resolved,

== Security

Security for the management endpoints exposed in the separate HTTP server needs to be enabled explicitly like in the example below:

[source, properties]
----
quarkus.management.enabled=true
quarkus.management.auth.enabled=true
----

Once enabled, you can use same authentication mechanism you have already configured for the main server, or use a different one.
All of these mechanisms are detailed in the xref:security-authentication-mechanisms.adoc[Authentication mechanisms in Quarkus] guide.

=== Use HTTP Security Policy to enable path-based authentication

The following configuration example demonstrates how you can enforce a single selectable authentication mechanism for a given request path:

[source,properties]
----
quarkus.management.auth.permission.metrics.paths=/q/metrics/*
quarkus.management.auth.permission.metrics.policy=authenticated
quarkus.management.auth.permission.metrics.auth-mechanism=basic <1>
quarkus.management.auth.permission.health.paths=/q/health/*
quarkus.management.auth.permission.health.policy=authenticated
quarkus.management.auth.permission.health.auth-mechanism=bearer <2>
----
<1> The metric endpoints will be only accessible with the <<basic-auth>>.
<2> If the Quarkus OIDC extension is present, the health endpoints will be authenticated
by the xref:security-oidc-bearer-token-authentication.adoc[OIDC Bearer token authentication].

[[basic-auth]]
=== Basic authentication

You can enable _basic_ authentication using the following properties:

[source, properties]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package io.quarkus.oidc.test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.IOException;

import jakarta.enterprise.event.Observes;
import jakarta.inject.Singleton;

import org.htmlunit.SilentCssErrorHandler;
import org.htmlunit.TextPage;
import org.htmlunit.WebClient;
import org.htmlunit.html.HtmlForm;
import org.htmlunit.html.HtmlPage;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.test.QuarkusDevModeTest;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager;
import io.quarkus.vertx.http.ManagementInterface;
import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser;

@QuarkusTestResource(KeycloakTestResourceLifecycleManager.class)
public class CodeFlowManagementInterfaceDevModeTest {

@RegisterExtension
static final QuarkusDevModeTest test = new QuarkusDevModeTest()
.withApplicationRoot((jar) -> jar
.addClasses(CodeFlowManagementRoute.class)
.addAsResource(
new StringAsset("""
quarkus.management.enabled=true
quarkus.management.auth.enabled=true
quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus
quarkus.oidc.client-id=quarkus-web-app
quarkus.oidc.credentials.secret=secret
quarkus.oidc.application-type=web-app
quarkus.management.auth.permission.code-flow.paths=/code-flow
quarkus.management.auth.permission.code-flow.policy=authenticated
quarkus.management.auth.permission.code-flow.auth-mechanism=code
quarkus.log.category."org.htmlunit".level=ERROR
quarkus.log.file.enable=true
"""),
"application.properties"));

@Test
public void testAuthenticatedHttpPermission() throws IOException {
try (final WebClient webClient = createWebClient()) {
HtmlPage page = webClient.getPage("http://0.0.0.0:9000/code-flow");

assertEquals("Sign in to quarkus", page.getTitleText());

HtmlForm loginForm = page.getForms().get(0);

loginForm.getInputByName("username").setValueAttribute("alice");
loginForm.getInputByName("password").setValueAttribute("alice");

TextPage textPage = loginForm.getInputByName("login").click();

assertEquals("alice", textPage.getContent());

webClient.getCookieManager().clearCookies();
}
}

private WebClient createWebClient() {
WebClient webClient = new WebClient();
webClient.setCssErrorHandler(new SilentCssErrorHandler());
return webClient;
}

@Singleton
public static class CodeFlowManagementRoute {
void setupManagementRoutes(@Observes ManagementInterface managementInterface, IdentityProviderManager ipm) {
managementInterface.router().get("/code-flow").handler(rc -> QuarkusHttpUser
.getSecurityIdentity(rc, ipm)
.map(i -> i.getPrincipal().getName())
.subscribe().with(rc::end, err -> rc.fail(500, err)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ void setupAuthenticationMechanisms(
void createManagementAuthMechHandler(ManagementInterfaceSecurityRecorder recorder, Capabilities capabilities,
ManagementInterfaceBuildTimeConfig buildTimeConfig,
BuildProducer<ManagementAuthenticationHandlerBuildItem> managementAuthMechHandlerProducer) {
if (buildTimeConfig.auth.basic.orElse(false) && capabilities.isPresent(Capability.SECURITY)) {
if (buildTimeConfig.auth.enabled && capabilities.isPresent(Capability.SECURITY)) {
managementAuthMechHandlerProducer.produce(new ManagementAuthenticationHandlerBuildItem(
recorder.managementAuthenticationHandler(buildTimeConfig.auth.proactive)));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package io.quarkus.vertx.http.security;

import static org.hamcrest.Matchers.equalTo;

import java.net.URL;
import java.util.function.Supplier;

import jakarta.enterprise.event.Observes;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.security.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.vertx.http.ManagementInterface;
import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser;
import io.restassured.RestAssured;

/**
* Tests that basic authentication is enabled for the management interface when no other
* mechanism is available.
*/
public class ManagementInterfaceBasicAuthTest {

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() {
@Override
public JavaArchive get() {
return ShrinkWrap.create(JavaArchive.class)
.addClasses(TestIdentityProvider.class, TestTrustedIdentityProvider.class, TestIdentityController.class,
ManagementPathHandler.class)
.addAsResource(new StringAsset("""
quarkus.management.enabled=true
quarkus.management.auth.enabled=true
quarkus.management.auth.policy.r1.roles-allowed=admin
quarkus.management.auth.permission.roles1.paths=/admin
quarkus.management.auth.permission.roles1.policy=r1
"""), "application.properties");
}
});

@TestHTTPResource(value = "/metrics", management = true)
URL metrics;

@BeforeAll
public static void setup() {
TestIdentityController.resetRoles()
.add("admin", "admin", "admin");
}

@Test
public void testBasicAuthSuccess() {
RestAssured
.given()
.auth().preemptive().basic("admin", "admin")
.redirects().follow(false)
.when()
.get(metrics)
.then()
.assertThat()
.statusCode(200)
.body(equalTo("admin:" + metrics.getPath()));

}

@Test
public void testBasicAuthFailure() {
RestAssured
.given()
.auth().preemptive().basic("admin", "wrongpassword")
.redirects().follow(false)
.get(metrics)
.then()
.assertThat()
.statusCode(401);

}

public static class ManagementPathHandler {

void setup(@Observes ManagementInterface mi) {
mi.router().get("/q/metrics").handler(event -> {
QuarkusHttpUser user = (QuarkusHttpUser) event.user();
StringBuilder ret = new StringBuilder();
if (user != null) {
ret.append(user.getSecurityIdentity().getPrincipal().getName());
}
ret.append(":");
ret.append(event.normalizedPath());
event.response().end(ret.toString());
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
*/
@ConfigGroup
public class ManagementAuthConfig {

/**
* If authentication for the management interface should be enabled.
*/
@ConfigItem(defaultValue = "${quarkus.management.auth.basic:false}")
public boolean enabled;

/**
* If basic auth should be enabled.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,12 @@ public void initializeAuthenticationHandler(RuntimeValue<AuthenticationHandler>

public Handler<RoutingContext> permissionCheckHandler() {
return new Handler<RoutingContext>() {
volatile ManagementInterfaceHttpAuthorizer authorizer;
private volatile ManagementInterfaceHttpAuthorizer authorizer;

@Override
public void handle(RoutingContext event) {
if (authorizer == null) {
if (authorizer == null) {
authorizer = CDI.current().select(ManagementInterfaceHttpAuthorizer.class).get();
}
authorizer = CDI.current().select(ManagementInterfaceHttpAuthorizer.class).get();
}
authorizer.checkPermission(event);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.quarkus.it.keycloak;

import jakarta.enterprise.event.Observes;

import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.vertx.http.ManagementInterface;
import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser;
import io.vertx.ext.web.RoutingContext;

public class ManagementInterfaceCustomRoute {

void init(@Observes ManagementInterface mi, IdentityProviderManager ipm) {
mi.router().get("/q/management-secured").handler(rc -> QuarkusHttpUser
.getSecurityIdentity(rc, ipm)
.map(i -> i.isAnonymous() ? "anonymous" : i.getPrincipal().getName())
.subscribe().with(rc::end, err -> fail(rc)));
mi.router().get("/q/management-public").handler(rc -> rc.end("this route is public"));
}

private static void fail(RoutingContext rc) {
rc.fail(500,
new IllegalStateException("This route must only be accessible by authenticated user with 'management' role"));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package io.quarkus.it.keycloak;

import java.net.URL;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;

import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;

import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;
import io.restassured.RestAssured;
import io.smallrye.jwt.algorithm.SignatureAlgorithm;
import io.smallrye.jwt.build.Jwt;

@TestProfile(BearerTokenManagementInterfaceTest.ManagementInterfaceProfile.class)
@QuarkusTest
public class BearerTokenManagementInterfaceTest {

@TestHTTPResource(value = "/management-secured", management = true)
URL managementSecured;

@TestHTTPResource(value = "/management-public", management = true)
URL managementPublic;

@Test
public void testPublicManagementRoute() {
// anonymous request to a public route -> success
RestAssured.given()
.when().get(managementPublic)
.then()
.statusCode(200)
.body(Matchers.is("this route is public"));
// route is public, but proactive auth is enabled, credentials are sent and RS256 is rejected
RestAssured.given().auth().oauth2(getAccessToken("admin", SignatureAlgorithm.RS256, "admin"))
.when().get(managementPublic)
.then()
.statusCode(401);
// PS256 is OK, 'management' roles is missing but no roles are required -> success
RestAssured.given().auth().oauth2(getAccessToken("admin", SignatureAlgorithm.PS256, "admin"))
.when().get(managementPublic)
.then()
.statusCode(200)
.body(Matchers.is("this route is public"));
}

@Test
public void testManagementRouteSecuredWithHttpPerm() {
// RS256 is rejected
RestAssured.given().auth().oauth2(getAccessToken("admin", SignatureAlgorithm.RS256, "admin"))
.when().get(managementSecured)
.then()
.statusCode(401);
// PS256 is OK but 'management' roles is missing
RestAssured.given().auth().oauth2(getAccessToken("admin", SignatureAlgorithm.PS256, "admin"))
.when().get(managementSecured)
.then()
.statusCode(403);
// PS256 is OK but 'management' roles is missing
RestAssured.given().auth().oauth2(getAccessToken("admin", SignatureAlgorithm.PS256, "management"))
.when().get(managementSecured)
.then()
.statusCode(200)
.body(Matchers.containsString("admin"));
}

@Test
public void testMainRouterAuthenticationWorks() {
// RS256 is rejected
RestAssured.given().auth().oauth2(getAccessToken("admin", SignatureAlgorithm.RS256, "admin"))
.when().get("/api/admin/bearer-required-algorithm")
.then()
.statusCode(401);
// PS256 is OK
RestAssured.given().auth().oauth2(getAccessToken("admin", SignatureAlgorithm.PS256, "admin"))
.when().get("/api/admin/bearer-required-algorithm")
.then()
.statusCode(200)
.body(Matchers.containsString("admin"));
}

private static String getAccessToken(String userName, SignatureAlgorithm alg, String... roles) {
return Jwt.preferredUserName(userName)
.groups(Set.copyOf(Arrays.asList(roles)))
.issuer("https://server.example.com")
.audience("https://service.example.com")
.jws().algorithm(alg)
.sign();
}

public static class ManagementInterfaceProfile implements QuarkusTestProfile {
@Override
public Map<String, String> getConfigOverrides() {
return Map.of(
"quarkus.management.enabled", "true",
"quarkus.management.auth.enabled", "true",
"quarkus.oidc.bearer-required-algorithm.tenant-paths", "*",
"quarkus.management.auth.permission.custom.paths", "/q/management-secured",
"quarkus.management.auth.permission.custom.policy", "management-policy",
"quarkus.management.auth.policy.management-policy.roles-allowed", "management");
}
}
}

0 comments on commit 05b92aa

Please sign in to comment.