diff --git a/build.gradle b/build.gradle index f78cccb3d0..29d9b6408d 100644 --- a/build.gradle +++ b/build.gradle @@ -188,7 +188,7 @@ project(':cruise-control-core') { implementation "org.slf4j:slf4j-api:1.7.36" implementation "org.apache.logging.log4j:log4j-slf4j-impl:2.17.2" implementation 'org.apache.commons:commons-math3:3.6.1' - api "org.eclipse.jetty:jetty-servlet:${jettyVersion}" + api "org.eclipse.jetty.ee10:jetty-ee10-servlet:${jettyVersion}" implementation 'com.github.spotbugs:spotbugs-annotations:4.8.6' api "io.vertx:vertx-core:${vertxVersion}" diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/KafkaCruiseControlServletApp.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/KafkaCruiseControlServletApp.java index 994aef3631..4e983925f9 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/KafkaCruiseControlServletApp.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/KafkaCruiseControlServletApp.java @@ -8,7 +8,7 @@ import com.linkedin.kafka.cruisecontrol.servlet.ServletRequestHandler; import com.linkedin.kafka.cruisecontrol.servlet.security.CruiseControlSecurityHandler; import com.linkedin.kafka.cruisecontrol.servlet.security.SecurityProvider; -import org.eclipse.jetty.security.ConstraintSecurityHandler; +import org.eclipse.jetty.ee10.servlet.security.ConstraintSecurityHandler; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.CustomRequestLog; import org.eclipse.jetty.server.HttpConfiguration; @@ -17,11 +17,11 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.Slf4jRequestLogWriter; -import org.eclipse.jetty.servlet.DefaultServlet; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.ee10.servlet.DefaultServlet; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; import org.eclipse.jetty.util.ssl.SslContextFactory; -import javax.servlet.ServletException; +import jakarta.servlet.ServletException; import java.util.List; public class KafkaCruiseControlServletApp extends KafkaCruiseControlApp { @@ -55,7 +55,7 @@ protected ServerConnector setupHttpConnector(String hostname, int port) { ServerConnector serverConnector; Boolean webserverSslEnable = _config.getBoolean(WebServerConfig.WEBSERVER_SSL_ENABLE_CONFIG); if (webserverSslEnable != null && webserverSslEnable) { - SslContextFactory sslServerContextFactory = new SslContextFactory.Server(); + SslContextFactory.Server sslServerContextFactory = new SslContextFactory.Server(); sslServerContextFactory.setKeyStorePath(_config.getString(WebServerConfig.WEBSERVER_SSL_KEYSTORE_LOCATION_CONFIG)); sslServerContextFactory.setKeyStorePassword(_config.getPassword(WebServerConfig.WEBSERVER_SSL_KEYSTORE_PASSWORD_CONFIG).value()); sslServerContextFactory.setKeyManagerPassword(_config.getPassword(WebServerConfig.WEBSERVER_SSL_KEY_PASSWORD_CONFIG).value()); @@ -124,7 +124,7 @@ protected void setupWebUi(ServletContextHandler contextHandler) { DefaultServlet defaultServlet = new DefaultServlet(); ServletHolder holderWebapp = new ServletHolder("default", defaultServlet); // holderWebapp.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false"); - holderWebapp.setInitParameter("resourceBase", webuiDir); + holderWebapp.setInitParameter("baseResource", webuiDir); contextHandler.addServlet(holderWebapp, webuiPathPrefix); } diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/KafkaCruiseControlUtils.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/KafkaCruiseControlUtils.java index f9460524d5..4a49e05d9e 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/KafkaCruiseControlUtils.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/KafkaCruiseControlUtils.java @@ -54,7 +54,7 @@ import org.apache.kafka.common.serialization.Deserializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.servlet.ServletException; +import jakarta.servlet.ServletException; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/async/progress/OperationProgress.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/async/progress/OperationProgress.java index 4475516859..a32721945a 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/async/progress/OperationProgress.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/async/progress/OperationProgress.java @@ -10,7 +10,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import javax.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSession; /** diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/monitor/sampling/prometheus/PrometheusAdapter.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/monitor/sampling/prometheus/PrometheusAdapter.java index 4f5e710b04..80d0d92419 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/monitor/sampling/prometheus/PrometheusAdapter.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/monitor/sampling/prometheus/PrometheusAdapter.java @@ -10,7 +10,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.io.IOUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpHost; diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/KafkaCruiseControlServletUtils.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/KafkaCruiseControlServletUtils.java index d6f7351220..977d1528a5 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/KafkaCruiseControlServletUtils.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/KafkaCruiseControlServletUtils.java @@ -17,19 +17,19 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import static com.linkedin.kafka.cruisecontrol.config.constants.CruiseControlParametersConfig.*; import static com.linkedin.kafka.cruisecontrol.config.constants.CruiseControlRequestConfig.*; import static com.linkedin.kafka.cruisecontrol.servlet.CruiseControlEndPoint.*; import static com.linkedin.kafka.cruisecontrol.servlet.parameters.ParameterUtils.*; import static com.linkedin.kafka.cruisecontrol.servlet.response.ResponseUtils.writeErrorResponse; -import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; -import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; -import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; -import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; -import static javax.servlet.http.HttpServletResponse.SC_OK; +import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; +import static jakarta.servlet.http.HttpServletResponse.SC_OK; /** diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/ServletRequestContext.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/ServletRequestContext.java index 3e33298b44..9bc6ed360d 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/ServletRequestContext.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/ServletRequestContext.java @@ -12,8 +12,8 @@ import com.linkedin.kafka.cruisecontrol.servlet.response.ResponseUtils; import io.vertx.core.MultiMap; import io.vertx.core.http.impl.headers.HeadersMultiMap; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/ServletRequestHandler.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/ServletRequestHandler.java index 0540425098..77462cb141 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/ServletRequestHandler.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/ServletRequestHandler.java @@ -8,9 +8,9 @@ import com.linkedin.kafka.cruisecontrol.KafkaCruiseControlEndPoints; import com.linkedin.kafka.cruisecontrol.KafkaCruiseControlRequestHandler; import com.linkedin.kafka.cruisecontrol.async.AsyncKafkaCruiseControl; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import static com.linkedin.kafka.cruisecontrol.servlet.KafkaCruiseControlServletUtils.handleOptions; diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/ServletSession.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/ServletSession.java index 8d4cbf69a4..4dc4a50774 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/ServletSession.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/ServletSession.java @@ -5,7 +5,7 @@ package com.linkedin.kafka.cruisecontrol.servlet; import com.linkedin.cruisecontrol.http.CruiseControlHttpSession; -import javax.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSession; import java.util.Objects; public class ServletSession implements CruiseControlHttpSession { diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/UserPermissionsManager.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/UserPermissionsManager.java index 9f4d4a9cba..8d72483d98 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/UserPermissionsManager.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/UserPermissionsManager.java @@ -4,6 +4,11 @@ package com.linkedin.kafka.cruisecontrol.servlet; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Set; import java.util.HashMap; @@ -11,11 +16,13 @@ import java.util.stream.Collectors; import java.util.Collections; import com.linkedin.kafka.cruisecontrol.config.KafkaCruiseControlConfig; +import org.eclipse.jetty.security.RolePrincipal; import org.eclipse.jetty.security.UserStore; import org.eclipse.jetty.security.PropertyUserStore; -import org.eclipse.jetty.security.AbstractLoginService; import com.linkedin.kafka.cruisecontrol.config.constants.WebServerConfig; -import javax.security.auth.Subject; +import org.eclipse.jetty.server.handler.ResourceHandler; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.resource.ResourceFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,23 +46,21 @@ public UserPermissionsManager(KafkaCruiseControlConfig config) { * @return a map of usernames -> their assigned roles */ private Map> createRolesPerUsersMap() { - Map> rolesPerUsers = new HashMap(); + Map> rolesPerUsers = new HashMap<>(); boolean securityEnabled = _config.getBoolean(WebServerConfig.WEBSERVER_SECURITY_ENABLE_CONFIG); if (securityEnabled) { - String privilegedFilePath = _config.getString(WebServerConfig.WEBSERVER_AUTH_CREDENTIALS_FILE_CONFIG); - UserStore userStore = createUserStoreFromFile(privilegedFilePath); + String privilegesFilePath = _config.getString(WebServerConfig.WEBSERVER_AUTH_CREDENTIALS_FILE_CONFIG); + Resource resource = ResourceFactory.of(new ResourceHandler()).newResource(privilegesFilePath); + UserStore userStore = createUserStoreFromResource(resource); startUserStore(userStore); - Set userNames = userStore.getKnownUserIdentities().keySet(); + Set userNames = parseUsernames(resource); for (String user : userNames) { - Subject userSubject = userStore.getUserIdentity(user).getSubject(); - Set roles = userSubject == null - ? new HashSet<>() - : userSubject.getPrincipals(AbstractLoginService.RolePrincipal.class); + Set roles = new HashSet<>(userStore.getRolePrincipals(user)); Set roleNames = roles.stream() - .map(AbstractLoginService.RolePrincipal::getName) + .map(RolePrincipal::getName) .map(String::toUpperCase) .collect(Collectors.toSet()); rolesPerUsers.put(user, roleNames); @@ -94,12 +99,43 @@ public Set getRolesBy(String userName) { /** Creates UserStore from an external file * - * @param privilegedFilePath a filepath containing user privileges information + * @param privilegedResource a filepath containing user privileges information * @return a UserStore object */ - private UserStore createUserStoreFromFile(String privilegedFilePath) { + private UserStore createUserStoreFromResource(Resource privilegedResource) { PropertyUserStore userStore = new PropertyUserStore(); - userStore.setConfig(privilegedFilePath); + userStore.setConfig(privilegedResource); return userStore; } + + /** Creates a set of usernames from a Resource + * + * @param resource a Resource containing user privileges information + * @return a Set of usernames parsed from the Resource + */ + private static Set parseUsernames(Resource resource) { + if (!resource.exists() || !resource.isReadable()) { + return Set.of(); + } + Set usernames = new HashSet<>(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.newInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + int colonIndex = line.indexOf(':'); + if (colonIndex != -1) { + String username = line.substring(0, colonIndex).trim(); + if (!username.isEmpty()) { + usernames.add(username); + } + } + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to read usernames from " + resource, e); + } + return usernames; + } } diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/UserTaskManager.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/UserTaskManager.java index 08e93b4052..21ecfaba62 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/UserTaskManager.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/UserTaskManager.java @@ -40,8 +40,8 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; import com.linkedin.kafka.cruisecontrol.servlet.response.JsonResponseClass; import com.linkedin.kafka.cruisecontrol.servlet.response.JsonResponseField; import org.apache.kafka.common.utils.Time; diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/parameters/AbstractParameters.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/parameters/AbstractParameters.java index 46438d3a48..d900b9e323 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/parameters/AbstractParameters.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/parameters/AbstractParameters.java @@ -13,7 +13,7 @@ import io.vertx.ext.web.RoutingContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.Collections; diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/parameters/ParameterUtils.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/parameters/ParameterUtils.java index 839e00f541..cf1dd64df9 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/parameters/ParameterUtils.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/parameters/ParameterUtils.java @@ -56,7 +56,7 @@ import static com.linkedin.kafka.cruisecontrol.servlet.purgatory.ReviewStatus.APPROVED; import static com.linkedin.kafka.cruisecontrol.servlet.purgatory.ReviewStatus.DISCARDED; import static com.linkedin.kafka.cruisecontrol.servlet.response.ResponseUtils.writeErrorResponse; -import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST; /** * The util class for Kafka Cruise Control parameters. diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/response/AbstractCruiseControlResponse.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/response/AbstractCruiseControlResponse.java index 129b5d24e7..b234217ffc 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/response/AbstractCruiseControlResponse.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/response/AbstractCruiseControlResponse.java @@ -10,7 +10,7 @@ import com.linkedin.cruisecontrol.http.CruiseControlRequestContext; import java.io.IOException; -import static javax.servlet.http.HttpServletResponse.SC_OK; +import static jakarta.servlet.http.HttpServletResponse.SC_OK; public abstract class AbstractCruiseControlResponse implements CruiseControlResponse { diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/response/ProgressResult.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/response/ProgressResult.java index 5a5a5055fc..036caf07c8 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/response/ProgressResult.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/response/ProgressResult.java @@ -18,7 +18,7 @@ import static com.linkedin.kafka.cruisecontrol.servlet.response.ResponseUtils.JSON_VERSION; import static com.linkedin.kafka.cruisecontrol.servlet.response.ResponseUtils.VERSION; -import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED; +import static jakarta.servlet.http.HttpServletResponse.SC_ACCEPTED; @JsonResponseClass public class ProgressResult extends AbstractCruiseControlResponse { diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/response/ResponseUtils.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/response/ResponseUtils.java index a75b9b8067..9a9f0d4993 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/response/ResponseUtils.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/response/ResponseUtils.java @@ -13,7 +13,7 @@ import com.linkedin.kafka.cruisecontrol.config.constants.WebServerConfig; import io.vertx.ext.web.RoutingContext; import org.apache.kafka.common.config.AbstractConfig; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/BasicSecurityProvider.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/BasicSecurityProvider.java index 747457b4ce..4b38b2c478 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/BasicSecurityProvider.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/BasicSecurityProvider.java @@ -10,6 +10,9 @@ import org.eclipse.jetty.security.HashLoginService; import org.eclipse.jetty.security.LoginService; import org.eclipse.jetty.security.authentication.BasicAuthenticator; +import org.eclipse.jetty.server.handler.ResourceHandler; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.resource.ResourceFactory; /** * This class defines a HTTP Basic authenticator with a file based {@link HashLoginService} and uses the default @@ -27,7 +30,8 @@ public void init(KafkaCruiseControlConfig config) { @Override public LoginService loginService() { - return new HashLoginService("DefaultLoginService", _userCredentialsFile); + Resource resource = ResourceFactory.of(new ResourceHandler()).newResource(_userCredentialsFile); + return new HashLoginService("DefaultLoginService", resource); } @Override diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/CruiseControlSecurityHandler.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/CruiseControlSecurityHandler.java index f1f3e134d1..c3ec4252c4 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/CruiseControlSecurityHandler.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/CruiseControlSecurityHandler.java @@ -4,12 +4,9 @@ package com.linkedin.kafka.cruisecontrol.servlet.security; -import org.eclipse.jetty.security.ConstraintSecurityHandler; +import org.eclipse.jetty.ee10.servlet.security.ConstraintSecurityHandler; +import org.eclipse.jetty.security.Constraint; import org.eclipse.jetty.server.Request; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; /** * A custom {@link ConstraintSecurityHandler} that converts the request to lowercase to ensure case insensitivity. @@ -17,8 +14,7 @@ public class CruiseControlSecurityHandler extends ConstraintSecurityHandler { @Override - public void handle(String pathInContext, Request baseRequest, HttpServletRequest request, HttpServletResponse response) - throws IOException, ServletException { - super.handle(pathInContext.toLowerCase(), baseRequest, request, response); + public Constraint getConstraint(String pathInContext, Request request) { + return super.getConstraint(pathInContext.toLowerCase(), request); } } diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/DefaultRoleSecurityProvider.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/DefaultRoleSecurityProvider.java index 47c03919a3..6e9bb6078d 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/DefaultRoleSecurityProvider.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/DefaultRoleSecurityProvider.java @@ -10,8 +10,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; -import org.eclipse.jetty.security.ConstraintMapping; -import org.eclipse.jetty.util.security.Constraint; +import org.eclipse.jetty.ee10.servlet.security.ConstraintMapping; +import org.eclipse.jetty.security.Constraint; /** @@ -65,10 +65,8 @@ public Set roles() { } private ConstraintMapping mapping(CruiseControlEndPoint endpoint, String... roles) { - Constraint constraint = new Constraint(); - constraint.setName(Constraint.__BASIC_AUTH); - constraint.setRoles(roles); - constraint.setAuthenticate(true); + Constraint.Builder builder = new Constraint.Builder(); + Constraint constraint = builder.roles(roles).name("BASIC").authorization(Constraint.Authorization.SPECIFIC_ROLE).build(); ConstraintMapping mapping = new ConstraintMapping(); mapping.setPathSpec(_webServerApiUrlPrefix.replace("*", endpoint.name().toLowerCase())); mapping.setConstraint(constraint); diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/DummyAuthorizationService.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/DummyAuthorizationService.java deleted file mode 100644 index 1b4b7e405e..0000000000 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/DummyAuthorizationService.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2023 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information. - */ - -package com.linkedin.kafka.cruisecontrol.servlet.security; - -import org.eclipse.jetty.security.SpnegoUserIdentity; -import org.eclipse.jetty.security.SpnegoUserPrincipal; -import org.eclipse.jetty.security.authentication.AuthorizationService; -import org.eclipse.jetty.server.UserIdentity; -import org.eclipse.jetty.util.security.Credential; -import javax.security.auth.Subject; -import javax.servlet.http.HttpServletRequest; -import java.security.Principal; - -public class DummyAuthorizationService implements AuthorizationService { - - private static final Credential NO_CREDENTIAL = new Credential() { - @Override - public boolean check(Object credentials) { - return false; - } - }; - - @Override - public UserIdentity getUserIdentity(HttpServletRequest request, String name) { - return createUserIdentity(name); - } - - private UserIdentity createUserIdentity(String username) { - Principal userPrincipal = new SpnegoUserPrincipal(username, ""); - Subject subject = new Subject(); - subject.getPrincipals().add(userPrincipal); - subject.getPrivateCredentials().add(NO_CREDENTIAL); - subject.setReadOnly(); - - return new SpnegoUserIdentity(subject, userPrincipal, null); - } - -} diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/DummyLoginService.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/DummyLoginService.java new file mode 100644 index 0000000000..aed38899f7 --- /dev/null +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/DummyLoginService.java @@ -0,0 +1,67 @@ +/* + * Copyright 2025 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information. + */ + +package com.linkedin.kafka.cruisecontrol.servlet.security; + +import org.eclipse.jetty.security.DefaultIdentityService; +import org.eclipse.jetty.security.IdentityService; +import org.eclipse.jetty.security.LoginService; +import org.eclipse.jetty.security.SPNEGOUserPrincipal; +import org.eclipse.jetty.security.UserIdentity; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Session; +import javax.security.auth.Subject; +import java.security.Principal; +import java.util.function.Function; + +import static com.linkedin.kafka.cruisecontrol.servlet.security.SecurityUtils.NO_CREDENTIAL; + +public class DummyLoginService implements LoginService { + private final IdentityService _identityService = new DefaultIdentityService(); + + @Override + public String getName() { + return "spnego-realm"; + } + + @Override + public IdentityService getIdentityService() { + return _identityService; + } + + @Override + public void setIdentityService(IdentityService svc) { + // dummy implementation + } + + @Override + public boolean validate(UserIdentity user) { + return true; + } + + @Override + public void logout(UserIdentity user) { + // dummy implementation + } + + @Override + public UserIdentity login(String u, Object c, Request req, Function getOrCreateSession) { + return null; + } + + @Override + public UserIdentity getUserIdentity(Subject subject, Principal userPrincipal, boolean create) { + return createUserIdentity(userPrincipal.getName()); + } + + private UserIdentity createUserIdentity(String username) { + Principal userPrincipal = new SPNEGOUserPrincipal(username, ""); + Subject subject = new Subject(); + subject.getPrincipals().add(userPrincipal); + subject.getPrivateCredentials().add(NO_CREDENTIAL); + subject.setReadOnly(); + + return _identityService.newUserIdentity(subject, userPrincipal, null); + } +} diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/RoleProvider.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/RoleProvider.java new file mode 100644 index 0000000000..87f5fa70b4 --- /dev/null +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/RoleProvider.java @@ -0,0 +1,21 @@ +/* + * Copyright 2025 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information. + */ + +package com.linkedin.kafka.cruisecontrol.servlet.security; + +import org.eclipse.jetty.server.Request; + +/** + * An interface to get roles for a given user. + */ +public interface RoleProvider { + /** + * Get the roles for a given user. + * + * @param request the request + * @param username the username + * @return the roles for the user or null if no roles found + */ + String[] rolesFor(Request request, String username); +} diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/SecurityProvider.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/SecurityProvider.java index a93f4a4b5d..b3d364e369 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/SecurityProvider.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/SecurityProvider.java @@ -6,9 +6,9 @@ import com.linkedin.kafka.cruisecontrol.config.KafkaCruiseControlConfig; import org.eclipse.jetty.security.Authenticator; -import org.eclipse.jetty.security.ConstraintMapping; +import org.eclipse.jetty.ee10.servlet.security.ConstraintMapping; import org.eclipse.jetty.security.LoginService; -import javax.servlet.ServletException; +import jakarta.servlet.ServletException; import java.util.List; import java.util.Set; @@ -35,7 +35,7 @@ public interface SecurityProvider { List constraintMappings(); /** - * Associates a username, credentials and roles with a {@link org.eclipse.jetty.server.UserIdentity} + * Associates a username, credentials and roles with a {@link org.eclipse.jetty.security.UserIdentity} * that will be used by Jetty to manage the authentication. * * @return a new {@link LoginService}. diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/UserStoreAuthorizationService.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/UserStoreAuthorizationService.java deleted file mode 100644 index 89f0b2c673..0000000000 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/UserStoreAuthorizationService.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2020 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information. - */ - -package com.linkedin.kafka.cruisecontrol.servlet.security; - -import org.eclipse.jetty.security.PropertyUserStore; -import org.eclipse.jetty.security.UserStore; -import org.eclipse.jetty.security.authentication.AuthorizationService; -import org.eclipse.jetty.server.UserIdentity; -import org.eclipse.jetty.util.component.AbstractLifeCycle; -import javax.servlet.http.HttpServletRequest; - -/** - * Can be used for authorization scenarios where a file can be created in a secure location with a relatively - * low number of users. It follows the username: password [,rolename ...] format which corresponds to - * the format used with {@link org.eclipse.jetty.security.HashLoginService}. - */ -public class UserStoreAuthorizationService extends AbstractLifeCycle implements AuthorizationService { - - private final UserStore _userStore; - - public UserStoreAuthorizationService(String privilegesFilePath) { - this(userStoreFromFile(privilegesFilePath)); - } - - public UserStoreAuthorizationService(UserStore userStore) { - _userStore = userStore; - } - - @Override - public UserIdentity getUserIdentity(HttpServletRequest request, String name) { - return _userStore.getUserIdentity(name); - } - - @Override - protected void doStart() throws Exception { - super.doStart(); - _userStore.start(); - } - - @Override - protected void doStop() throws Exception { - _userStore.stop(); - super.doStop(); - } - - private static UserStore userStoreFromFile(String privilegesFilePath) { - PropertyUserStore userStore = new PropertyUserStore(); - userStore.setConfig(privilegesFilePath); - return userStore; - } -} diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/UserStoreRoleProvider.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/UserStoreRoleProvider.java new file mode 100644 index 0000000000..3382589b7b --- /dev/null +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/UserStoreRoleProvider.java @@ -0,0 +1,61 @@ +/* + * Copyright 2025 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information. + */ + +package com.linkedin.kafka.cruisecontrol.servlet.security; + +import org.eclipse.jetty.security.PropertyUserStore; +import org.eclipse.jetty.security.RolePrincipal; +import org.eclipse.jetty.security.UserStore; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.resource.PathResourceFactory; +import org.eclipse.jetty.util.resource.Resource; +import java.nio.file.Path; +import java.util.List; + +public class UserStoreRoleProvider extends AbstractLifeCycle implements RoleProvider { + + private final UserStore _userStore; + + public UserStoreRoleProvider(UserStore userStore) { + _userStore = userStore; + } + + public UserStoreRoleProvider(String privilegesFilePath) { + PropertyUserStore store = new PropertyUserStore(); + Resource res = new PathResourceFactory().newResource(Path.of(privilegesFilePath).toUri()); + store.setConfig(res); + _userStore = store; + } + + /** + * Get the roles for a given user. + * + * @param request the request + * @param username the username + * @return the roles for the user or null if no roles found + */ + public String[] rolesFor(Request request, String username) { + List rolePrincipals = _userStore.getRolePrincipals(username); + if (rolePrincipals == null || rolePrincipals.isEmpty()) { + return null; + } + return rolePrincipals.stream().map(RolePrincipal::getName).toArray(String[]::new); + } + + @Override + protected void doStart() throws Exception { + super.doStart(); + _userStore.start(); + } + + @Override + protected void doStop() throws Exception { + try { + _userStore.stop(); + } finally { + super.doStop(); + } + } +} diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtAuthenticator.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtAuthenticator.java index c0e50148fd..f515bf6769 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtAuthenticator.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtAuthenticator.java @@ -5,23 +5,23 @@ package com.linkedin.kafka.cruisecontrol.servlet.security.jwt; import com.nimbusds.jwt.SignedJWT; +import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.security.AuthenticationState; import org.eclipse.jetty.security.ServerAuthException; -import org.eclipse.jetty.security.UserAuthentication; +import org.eclipse.jetty.security.UserIdentity; import org.eclipse.jetty.security.authentication.LoginAuthenticator; -import org.eclipse.jetty.server.Authentication; -import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.URIUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; import java.text.ParseException; +import java.util.List; import java.util.function.Function; /** @@ -58,7 +58,7 @@ public class JwtAuthenticator extends LoginAuthenticator { static final String REDIRECT_URL = "{redirectUrl}"; private final String _cookieName; - private final Function _authenticationProviderUrlGenerator; + private final Function _authenticationProviderUrlGenerator; /** * Creates a new {@link JwtAuthenticator} instance with a custom authentication provider url and a cookie name that @@ -73,84 +73,61 @@ public class JwtAuthenticator extends LoginAuthenticator { */ public JwtAuthenticator(String authenticationProviderUrl, String cookieName) { _cookieName = cookieName; - Function> urlGen = - url -> req -> url.replace(REDIRECT_URL, req.getRequestURL().toString() + getOriginalQueryString(req)); + Function> urlGen = + url -> req -> url.replace(REDIRECT_URL, getRequestURL(req) + getOriginalQueryString(req)); _authenticationProviderUrlGenerator = urlGen.apply(authenticationProviderUrl); } @Override - public String getAuthMethod() { + public String getAuthenticationType() { return METHOD; } @Override - public void prepareRequest(ServletRequest request) { - - } - - @Override - public Authentication validateRequest(ServletRequest request, ServletResponse response, boolean mandatory) throws ServerAuthException { + public AuthenticationState validateRequest(Request request, Response response, Callback callback) throws ServerAuthException { JWT_LOGGER.trace("Authentication request received for " + request.toString()); - if (!(request instanceof HttpServletRequest) && !(response instanceof HttpServletResponse)) { - return Authentication.UNAUTHENTICATED; - } String serializedJWT; - HttpServletRequest req = (HttpServletRequest) request; // we'll skip the authentication for CORS preflight requests - if (HttpMethod.OPTIONS.name().equalsIgnoreCase(req.getMethod())) { - return Authentication.NOT_CHECKED; + if (HttpMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod())) { + return null; } - serializedJWT = getJwtFromBearerAuthorization(req); + serializedJWT = getJwtFromBearerAuthorization(request); if (serializedJWT == null) { - serializedJWT = getJwtFromCookie(req); + serializedJWT = getJwtFromCookie(request); } if (serializedJWT == null) { - String loginURL = _authenticationProviderUrlGenerator.apply(req); + String loginURL = _authenticationProviderUrlGenerator.apply(request); JWT_LOGGER.info("No JWT token found, sending redirect to " + loginURL); - try { - ((HttpServletResponse) response).sendRedirect(loginURL); - return Authentication.SEND_CONTINUE; - } catch (IOException e) { - JWT_LOGGER.error("Couldn't authenticate request", e); - throw new ServerAuthException(e); - } + Response.sendRedirect(request, response, callback, loginURL); + return AuthenticationState.CHALLENGE; } else { try { SignedJWT jwtToken = SignedJWT.parse(serializedJWT); String userName = jwtToken.getJWTClaimsSet().getSubject(); request.setAttribute(JWT_TOKEN_REQUEST_ATTRIBUTE, serializedJWT); - UserIdentity identity = login(userName, jwtToken, request); + UserIdentity identity = login(userName, jwtToken, request, response); if (identity == null) { - ((HttpServletResponse) response).setStatus(HttpStatus.UNAUTHORIZED_401); - return Authentication.SEND_FAILURE; + response.setStatus(HttpStatus.UNAUTHORIZED_401); + return AuthenticationState.SEND_FAILURE; } else { - return new UserAuthentication(getAuthMethod(), identity); + return new UserAuthenticationSucceeded(getAuthenticationType(), identity); } } catch (ParseException pe) { - String loginURL = _authenticationProviderUrlGenerator.apply(req); + String loginURL = _authenticationProviderUrlGenerator.apply(request); JWT_LOGGER.warn("Unable to parse the JWT token, redirecting back to the login page", pe); - try { - ((HttpServletResponse) response).sendRedirect(loginURL); - } catch (IOException e) { - throw new ServerAuthException(e); - } + Response.sendRedirect(request, response, callback, loginURL); } } - return Authentication.SEND_FAILURE; + return AuthenticationState.SEND_FAILURE; } - @Override - public boolean secureResponse(ServletRequest request, ServletResponse response, boolean mandatory, Authentication.User validatedUser) { - return true; - } - - String getJwtFromCookie(HttpServletRequest req) { + String getJwtFromCookie(Request req) { String serializedJWT = null; - Cookie[] cookies = req.getCookies(); + List cookies = Request.getCookies(req); if (cookies != null) { - for (Cookie cookie : cookies) { + for (HttpCookie cookie : cookies) { if (_cookieName != null && _cookieName.equals(cookie.getName())) { JWT_LOGGER.trace(_cookieName + " cookie has been found and is being processed"); serializedJWT = cookie.getValue(); @@ -161,8 +138,8 @@ String getJwtFromCookie(HttpServletRequest req) { return serializedJWT; } - String getJwtFromBearerAuthorization(HttpServletRequest req) { - String authorizationHeader = req.getHeader(HttpHeader.AUTHORIZATION.asString()); + String getJwtFromBearerAuthorization(Request req) { + String authorizationHeader = req.getHeaders().get(HttpHeader.AUTHORIZATION.asString()); if (authorizationHeader == null || !authorizationHeader.startsWith(BEARER)) { return null; } else { @@ -170,8 +147,25 @@ String getJwtFromBearerAuthorization(HttpServletRequest req) { } } - private String getOriginalQueryString(HttpServletRequest request) { - String originalQueryString = request.getQueryString(); + private String getOriginalQueryString(Request req) { + String originalQueryString = req.getHttpURI().getQuery(); return (originalQueryString == null) ? "" : "?" + originalQueryString; } + + /** + * Get the full request URL including scheme, host, port and path but excluding the query string. + * + * @param req is the request to process + * @return the full request URL + */ + public String getRequestURL(Request req) { + final StringBuilder url = new StringBuilder(); + HttpURI uri = req.getHttpURI(); + URIUtil.appendSchemeHostPort(url, uri.getScheme(), Request.getServerName(req), Request.getServerPort(req)); + String path = uri.getPath(); + if (path != null) { + url.append(path); + } + return url.toString(); + } } diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtLoginService.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtLoginService.java index ec1187a1fe..3a383f3cea 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtLoginService.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtLoginService.java @@ -4,7 +4,8 @@ package com.linkedin.kafka.cruisecontrol.servlet.security.jwt; -import com.linkedin.kafka.cruisecontrol.servlet.security.UserStoreAuthorizationService; +import com.linkedin.kafka.cruisecontrol.servlet.security.RoleProvider; +import com.linkedin.kafka.cruisecontrol.servlet.security.UserStoreRoleProvider; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSObject; import com.nimbusds.jose.JWSVerifier; @@ -14,13 +15,12 @@ import org.eclipse.jetty.security.DefaultIdentityService; import org.eclipse.jetty.security.IdentityService; import org.eclipse.jetty.security.LoginService; -import org.eclipse.jetty.security.authentication.AuthorizationService; -import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.security.UserIdentity; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Session; import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.eclipse.jetty.util.component.LifeCycle; import javax.security.auth.Subject; -import javax.servlet.ServletRequest; -import javax.servlet.http.HttpServletRequest; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.file.Files; @@ -36,41 +36,42 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.Function; import static com.linkedin.kafka.cruisecontrol.servlet.security.jwt.JwtAuthenticator.JWT_LOGGER; /** - *

This class validates a JWT token. The token must be cryptographically encrypted and it uses an RSA public key for + *

This class validates a JWT token. The token must be cryptographically encrypted, and it uses an RSA public key for * validation that is expected to be stored in a PEM formatted file.

- *

This class implements {@link AbstractLifeCycle} which means it is a managed bean, its lifecycle will be managed - * by Jetty. It's {@link AuthorizationService} can also be an {@link AbstractLifeCycle} in which case it delegates to + *

This class implements {@link AbstractLifeCycle} which means it is a managed bean, jetty will manage its lifecycle. + * It's {@link RoleProvider} can also be an {@link AbstractLifeCycle} in which case it delegates to * this class, so opening and closing connections should be done by implementing the {@link AbstractLifeCycle} interface's * {@link #doStart()} and {@link #doStop()} methods respectively. For a simple example see - * {@link UserStoreAuthorizationService}.

- *

The login service also validates expiration time of the token and it expects the token to contain the expiration + * {@link UserStoreRoleProvider}.

+ *

The login service also validates expiration time of the token, and it expects the token to contain the expiration * in Unix epoch time format in UTC.

*/ public class JwtLoginService extends AbstractLifeCycle implements LoginService { public static final String X_509_CERT_TYPE = "X.509"; - private final AuthorizationService _authorizationService; + private final RoleProvider _roleProvider; private IdentityService _identityService; private final RSAPublicKey _publicKey; private final List _audiences; private Clock _clock; - public JwtLoginService(AuthorizationService authorizationService, String publicKeyLocation, List audiences) + public JwtLoginService(RoleProvider roleProvider, String publicKeyLocation, List audiences) throws IOException, CertificateException { - this(authorizationService, readPublicKey(publicKeyLocation), audiences); + this(roleProvider, readPublicKey(publicKeyLocation), audiences); } - public JwtLoginService(AuthorizationService authorizationService, RSAPublicKey publicKey, List audiences) { - this(authorizationService, publicKey, audiences, Clock.systemUTC()); + public JwtLoginService(RoleProvider roleProvider, RSAPublicKey publicKey, List audiences) { + this(roleProvider, publicKey, audiences, Clock.systemUTC()); } - public JwtLoginService(AuthorizationService authorizationService, RSAPublicKey publicKey, List audiences, Clock clock) { - _authorizationService = authorizationService; + public JwtLoginService(RoleProvider roleProvider, RSAPublicKey publicKey, List audiences, Clock clock) { _identityService = new DefaultIdentityService(); + _roleProvider = roleProvider; _publicKey = publicKey; _audiences = audiences; _clock = clock; @@ -79,16 +80,15 @@ public JwtLoginService(AuthorizationService authorizationService, RSAPublicKey p @Override protected void doStart() throws Exception { super.doStart(); - // The authorization service might want to start a connection or access a file - if (_authorizationService instanceof LifeCycle) { - ((LifeCycle) _authorizationService).start(); + if (_roleProvider instanceof LifeCycle) { + ((LifeCycle) _roleProvider).start(); } } @Override protected void doStop() throws Exception { - if (_authorizationService instanceof LifeCycle) { - ((LifeCycle) _authorizationService).stop(); + if (_roleProvider instanceof LifeCycle) { + ((LifeCycle) _roleProvider).stop(); } super.doStop(); } @@ -99,13 +99,10 @@ public String getName() { } @Override - public UserIdentity login(String username, Object credentials, ServletRequest request) { + public UserIdentity login(String username, Object credentials, Request request, Function getOrCreateSession) { if (!(credentials instanceof SignedJWT)) { return null; } - if (!(request instanceof HttpServletRequest)) { - return null; - } SignedJWT jwtToken = (SignedJWT) credentials; JWTClaimsSet claimsSet; @@ -117,17 +114,15 @@ public UserIdentity login(String username, Object credentials, ServletRequest re JWT_LOGGER.warn(String.format("%s: Couldn't parse a JWT token", username), e); return null; } - if (valid) { - String serializedToken = (String) request.getAttribute(JwtAuthenticator.JWT_TOKEN_REQUEST_ATTRIBUTE); - UserIdentity rolesDelegate = _authorizationService.getUserIdentity((HttpServletRequest) request, username); - if (rolesDelegate == null) { - return null; - } else { - return getUserIdentity(jwtToken, claimsSet, serializedToken, username, rolesDelegate); - } - } else { + if (!valid) { return null; } + String[] roles = _roleProvider.rolesFor(request, username); + if (roles == null) { + return null; + } + String serializedToken = (String) request.getAttribute(JwtAuthenticator.JWT_TOKEN_REQUEST_ATTRIBUTE); + return getUserIdentity(jwtToken, claimsSet, serializedToken, username, roles); } @Override @@ -213,13 +208,13 @@ private static RSAPublicKey readPublicKey(String location) throws CertificateExc return (RSAPublicKey) cer.getPublicKey(); } - private static UserIdentity getUserIdentity(SignedJWT jwtToken, JWTClaimsSet claimsSet, String serializedToken, - String username, UserIdentity rolesDelegate) { + private UserIdentity getUserIdentity(SignedJWT jwtToken, JWTClaimsSet claimsSet, String serializedToken, + String username, String[] roles) { JwtUserPrincipal principal = new JwtUserPrincipal(username, serializedToken); Set privCreds = new HashSet<>(); privCreds.add(jwtToken); privCreds.add(claimsSet); Subject subject = new Subject(true, Collections.singleton(principal), Collections.emptySet(), privCreds); - return new JwtUserIdentity(subject, principal, rolesDelegate); + return new JwtUserIdentity(subject, principal, roles); } } diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtSecurityProvider.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtSecurityProvider.java index 5100060b94..154c5a5092 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtSecurityProvider.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtSecurityProvider.java @@ -7,11 +7,10 @@ import com.linkedin.kafka.cruisecontrol.config.KafkaCruiseControlConfig; import com.linkedin.kafka.cruisecontrol.config.constants.WebServerConfig; import com.linkedin.kafka.cruisecontrol.servlet.security.DefaultRoleSecurityProvider; -import com.linkedin.kafka.cruisecontrol.servlet.security.UserStoreAuthorizationService; +import com.linkedin.kafka.cruisecontrol.servlet.security.UserStoreRoleProvider; +import jakarta.servlet.ServletException; import org.eclipse.jetty.security.Authenticator; import org.eclipse.jetty.security.LoginService; -import org.eclipse.jetty.security.authentication.AuthorizationService; -import javax.servlet.ServletException; import java.io.IOException; import java.security.cert.CertificateException; import java.util.List; @@ -49,7 +48,7 @@ public void init(KafkaCruiseControlConfig config) { @Override public LoginService loginService() throws ServletException { try { - return new JwtLoginService(authorizationService(), _publicKeyLocation, _audiences); + return new JwtLoginService(roleProvider(), _publicKeyLocation, _audiences); } catch (IOException | CertificateException e) { throw new ServletException(e); } @@ -60,7 +59,12 @@ public Authenticator authenticator() { return new JwtAuthenticator(_authenticationProviderUrl, _cookieName); } - public AuthorizationService authorizationService() { - return new UserStoreAuthorizationService(_privilegesFilePath); + /** + * Gets the role provider for JWT authentication. + * + * @return UserStoreRoleProvider + */ + public UserStoreRoleProvider roleProvider() { + return new UserStoreRoleProvider(_privilegesFilePath); } } diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtUserIdentity.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtUserIdentity.java index 302ba29049..71c772fbbe 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtUserIdentity.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtUserIdentity.java @@ -4,20 +4,21 @@ package com.linkedin.kafka.cruisecontrol.servlet.security.jwt; -import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.security.UserIdentity; import javax.security.auth.Subject; import java.security.Principal; +import java.util.Arrays; public class JwtUserIdentity implements UserIdentity { private final Subject _subject; private final Principal _principal; - private final UserIdentity _roleDelegate; + private final String[] _roles; - JwtUserIdentity(Subject subject, Principal principal, UserIdentity roleDelegate) { + JwtUserIdentity(Subject subject, Principal principal, String[] roles) { _subject = subject; _principal = principal; - _roleDelegate = roleDelegate; + _roles = roles; } @Override @@ -31,7 +32,7 @@ public Principal getUserPrincipal() { } @Override - public boolean isUserInRole(String role, Scope scope) { - return _roleDelegate != null && _roleDelegate.isUserInRole(role, scope); + public boolean isUserInRole(String role) { + return _roles != null && Arrays.asList(_roles).contains(role); } } diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoLoginServiceWithAuthServiceLifecycle.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoLoginServiceWithAuthServiceLifecycle.java index d5077eaf9c..ae4c5b8a9e 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoLoginServiceWithAuthServiceLifecycle.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoLoginServiceWithAuthServiceLifecycle.java @@ -4,18 +4,20 @@ package com.linkedin.kafka.cruisecontrol.servlet.security.spnego; -import com.linkedin.kafka.cruisecontrol.servlet.security.DummyAuthorizationService; -import com.linkedin.kafka.cruisecontrol.servlet.security.UserStoreAuthorizationService; +import com.linkedin.kafka.cruisecontrol.servlet.security.DummyLoginService; +import com.linkedin.kafka.cruisecontrol.servlet.security.RoleProvider; +import com.linkedin.kafka.cruisecontrol.servlet.security.UserStoreRoleProvider; import org.apache.kafka.common.security.kerberos.KerberosName; import org.apache.kafka.common.security.kerberos.KerberosShortNamer; -import org.eclipse.jetty.security.ConfigurableSpnegoLoginService; import org.eclipse.jetty.security.IdentityService; import org.eclipse.jetty.security.LoginService; import org.eclipse.jetty.security.PropertyUserStore; -import org.eclipse.jetty.security.SpnegoUserIdentity; -import org.eclipse.jetty.security.SpnegoUserPrincipal; -import org.eclipse.jetty.security.authentication.AuthorizationService; -import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.security.RoleDelegateUserIdentity; +import org.eclipse.jetty.security.SPNEGOLoginService; +import org.eclipse.jetty.security.SPNEGOUserPrincipal; +import org.eclipse.jetty.security.UserIdentity; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Session; import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.ietf.jgss.GSSContext; import org.ietf.jgss.GSSCredential; @@ -24,9 +26,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.security.auth.Subject; -import javax.servlet.ServletRequest; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -34,30 +33,31 @@ import java.nio.file.Path; import java.security.PrivilegedAction; import java.util.List; +import java.util.function.Function; /** - * This class is purely needed in order to manage the {@link AuthorizationService}. - * For instance if the AuthorizationService holds a {@link org.eclipse.jetty.security.PropertyUserStore} then it would load + * This class is purely needed to manage the {@link RoleProvider}. + * For instance if the RoleProvider holds a {@link org.eclipse.jetty.security.PropertyUserStore} then it would load * users from the store during the {@link PropertyUserStore#start()} method. * - * @see UserStoreAuthorizationService + * @see UserStoreRoleProvider */ public class SpnegoLoginServiceWithAuthServiceLifecycle extends ContainerLifeCycle implements LoginService { private static final Logger LOG = LoggerFactory.getLogger(SpnegoLoginServiceWithAuthServiceLifecycle.class); - private static final String GSS_HOLDER_CLASS_NAME = "org.eclipse.jetty.security.ConfigurableSpnegoLoginService$GSSContextHolder"; + private static final String GSS_HOLDER_CLASS_NAME = "org.eclipse.jetty.security.SPNEGOLoginService$GSSContextHolder"; private static final String REQUEST_ATTR_ADDED_NEW_SESSION = SpnegoLoginServiceWithAuthServiceLifecycle.class.getName() + "#ADDED_NEW_SESSION"; private final GSSManager _gssManager = GSSManager.getInstance(); - private final ConfigurableSpnegoLoginService _spnegoLoginService; - private final AuthorizationService _authorizationService; + private final SPNEGOLoginService _spnegoLoginService; + private final RoleProvider _roleProvider; private final KerberosShortNamer _kerberosShortNamer; private Subject _spnegoSubject; private GSSCredential _spnegoServiceCredential; private Constructor _holderConstructor; - public SpnegoLoginServiceWithAuthServiceLifecycle(String realm, AuthorizationService authorizationService, List principalToLocalRules) { - _spnegoLoginService = new ConfigurableSpnegoLoginService(realm, new DummyAuthorizationService()); - _authorizationService = authorizationService; + public SpnegoLoginServiceWithAuthServiceLifecycle(String realm, RoleProvider roleProvider, List principalToLocalRules) { + _spnegoLoginService = new SPNEGOLoginService(realm, new DummyLoginService()); + _roleProvider = roleProvider; _kerberosShortNamer = principalToLocalRules == null || principalToLocalRules.isEmpty() ? null : KerberosShortNamer.fromUnparsedRules(realm, principalToLocalRules); @@ -66,7 +66,7 @@ public SpnegoLoginServiceWithAuthServiceLifecycle(String realm, AuthorizationSer @Override protected void doStart() throws Exception { addBean(_spnegoLoginService); - addBean(_authorizationService); + addBean(_roleProvider); super.doStart(); extractSpnegoContext(); } @@ -77,25 +77,43 @@ public String getName() { } @Override - public UserIdentity login(String username, Object credentials, ServletRequest req) { + public UserIdentity login(String username, Object credentials, Request request, Function getOrCreateSession) { // save GSS context - HttpServletRequest request = (HttpServletRequest) req; GSSContext gssContext = addContext(request); // authentication - SpnegoUserIdentity userIdentity = (SpnegoUserIdentity) _spnegoLoginService.login(username, credentials, req); - SpnegoUserPrincipal userPrincipal = (SpnegoUserPrincipal) userIdentity.getUserPrincipal(); + UserIdentity userIdentity = _spnegoLoginService.login(username, credentials, request, getOrCreateSession); + if (userIdentity == null) { + return null; + } + if (userIdentity instanceof RoleDelegateUserIdentity) { + return userIdentity; + } // get full principal and create user principal shortname String fullPrincipal = getFullPrincipalFromGssContext(gssContext); cleanRequest(request); - LOG.debug("User {} logged in with full principal {}", userPrincipal.getName(), fullPrincipal); String userShortname = getSpnegoUserPrincipalShortname(fullPrincipal); + SPNEGOUserPrincipal orig; + if (userIdentity.getUserPrincipal() instanceof SPNEGOUserPrincipal) { + orig = (SPNEGOUserPrincipal) userIdentity.getUserPrincipal(); + } else { + orig = null; + } + + SPNEGOUserPrincipal finalPrincipal = new SPNEGOUserPrincipal(userShortname, orig != null ? orig.getEncodedToken() : null); + + LOG.debug("User {} logged in with full principal {}", finalPrincipal.getName(), fullPrincipal); + // do authorization and create UserIdentity - userPrincipal = new SpnegoUserPrincipal(userShortname, userPrincipal.getEncodedToken()); - UserIdentity roleDelegate = _authorizationService.getUserIdentity((HttpServletRequest) req, userShortname); - return new SpnegoUserIdentity(userIdentity.getSubject(), userPrincipal, roleDelegate); + String[] roles = _roleProvider.rolesFor(request, userShortname); + if (roles == null) { + return new RoleDelegateUserIdentity(userIdentity.getSubject(), finalPrincipal, null); + } + IdentityService is = getIdentityService(); + UserIdentity delegate = is.newUserIdentity(userIdentity.getSubject(), finalPrincipal, roles); + return new RoleDelegateUserIdentity(userIdentity.getSubject(), finalPrincipal, delegate); } @Override @@ -130,19 +148,6 @@ public void setKeyTabPath(Path keyTabFile) { _spnegoLoginService.setKeyTabPath(keyTabFile); } - private String getFullPrincipal(HttpServletRequest request) { - String fullPrincipal; - try { - fullPrincipal = ((GSSContext) request.getSession() - .getAttribute(GSS_HOLDER_CLASS_NAME)) - .getSrcName() - .toString(); - } catch (GSSException e) { - throw new RuntimeException(e); - } - return fullPrincipal; - } - private String getSpnegoUserPrincipalShortname(String fullPrincipal) { PrincipalName userPrincipalName = PrincipalValidator.parsePrincipal("", fullPrincipal); @@ -160,7 +165,7 @@ private String getSpnegoUserPrincipalShortname(String fullPrincipal) { } } - private String getFullPrincipalFromGssContext(GSSContext gssContext) { + protected String getFullPrincipalFromGssContext(GSSContext gssContext) { try { return gssContext.getSrcName().toString(); } catch (GSSException e) { @@ -170,11 +175,11 @@ private String getFullPrincipalFromGssContext(GSSContext gssContext) { // Visible for testing void extractSpnegoContext() { - // All the following code depends on the structure of org.eclipse.jetty.security.ConfigurableSpnegoLoginService$GSSContextHolder and - // org.eclipse.jetty.security.ConfigurableSpnegoLoginService$SpnegoContext + // All the following code depends on the structure of org.eclipse.jetty.security.SPNEGOLoginService$GSSContextHolder and + // org.eclipse.jetty.security.SPNEGOLoginService$SpnegoContext // If jetty is upgraded, this code might brake try { - Field contextField = ConfigurableSpnegoLoginService.class.getDeclaredField("_context"); + Field contextField = SPNEGOLoginService.class.getDeclaredField("_context"); contextField.setAccessible(true); Object spnegoContext = contextField.get(_spnegoLoginService); Class contextClass = spnegoContext.getClass(); @@ -190,12 +195,11 @@ void extractSpnegoContext() { } catch (NoSuchFieldException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException e) { throw new RuntimeException("Failed to init SPNEGO context", e); } - } - private GSSContext addContext(HttpServletRequest request) { - // This tries to inject an externally created GSSContext into ConfigurableSpnegoLoginService. - // ConfigurableSpnegoLoginService drops the realm part of the client principal, but we need that to evaluate the auth to local rules + protected GSSContext addContext(Request request) { + // This tries to inject an externally created GSSContext into SPNEGOLoginService. + // SPNEGOLoginService drops the realm part of the client principal, but we need that to evaluate the auth to local rules // By injecting the GSSContext through the session, we can get access to the full principal // If jetty is upgraded, this code might brake try { @@ -212,12 +216,12 @@ private GSSContext addContext(HttpServletRequest request) { } } - private void cleanRequest(HttpServletRequest request) { + private void cleanRequest(Request request) { if (!"true".equals(request.getAttribute(REQUEST_ATTR_ADDED_NEW_SESSION))) { return; } request.removeAttribute(REQUEST_ATTR_ADDED_NEW_SESSION); - HttpSession session = request.getSession(); + Session session = request.getSession(false); if (session != null) { try { session.invalidate(); diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoSecurityProvider.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoSecurityProvider.java index 972a97f4e4..575bada709 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoSecurityProvider.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoSecurityProvider.java @@ -7,11 +7,11 @@ import com.linkedin.kafka.cruisecontrol.config.KafkaCruiseControlConfig; import com.linkedin.kafka.cruisecontrol.config.constants.WebServerConfig; import com.linkedin.kafka.cruisecontrol.servlet.security.DefaultRoleSecurityProvider; +import com.linkedin.kafka.cruisecontrol.servlet.security.RoleProvider; import org.apache.kafka.common.security.kerberos.KerberosName; import org.eclipse.jetty.security.Authenticator; import org.eclipse.jetty.security.LoginService; -import org.eclipse.jetty.security.authentication.AuthorizationService; -import org.eclipse.jetty.security.authentication.ConfigurableSpnegoAuthenticator; +import org.eclipse.jetty.security.authentication.SPNEGOAuthenticator; import java.nio.file.Paths; import java.util.List; @@ -37,7 +37,7 @@ public void init(KafkaCruiseControlConfig config) { @Override public LoginService loginService() { SpnegoLoginServiceWithAuthServiceLifecycle loginService = new SpnegoLoginServiceWithAuthServiceLifecycle( - _spnegoPrincipal.realm(), authorizationService(), _spnegoPrincipalToLocalRules); + _spnegoPrincipal.realm(), roleProvider(), _spnegoPrincipalToLocalRules); loginService.setServiceName(_spnegoPrincipal.serviceName()); loginService.setHostName(_spnegoPrincipal.hostName()); loginService.setKeyTabPath(Paths.get(_keyTabPath)); @@ -46,10 +46,10 @@ public LoginService loginService() { @Override public Authenticator authenticator() { - return new ConfigurableSpnegoAuthenticator(); + return new SPNEGOAuthenticator(); } - public AuthorizationService authorizationService() { - return new SpnegoUserStoreAuthorizationService(_privilegesFilePath); + public RoleProvider roleProvider() { + return new SpnegoUserStoreRoleProvider(_privilegesFilePath); } } diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoUserStoreAuthorizationService.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoUserStoreRoleProvider.java similarity index 58% rename from cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoUserStoreAuthorizationService.java rename to cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoUserStoreRoleProvider.java index 9442842350..1d8c6d3378 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoUserStoreAuthorizationService.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoUserStoreRoleProvider.java @@ -4,27 +4,26 @@ package com.linkedin.kafka.cruisecontrol.servlet.security.spnego; -import com.linkedin.kafka.cruisecontrol.servlet.security.UserStoreAuthorizationService; +import com.linkedin.kafka.cruisecontrol.servlet.security.UserStoreRoleProvider; import org.eclipse.jetty.security.UserStore; -import org.eclipse.jetty.server.UserIdentity; -import javax.servlet.http.HttpServletRequest; +import org.eclipse.jetty.server.Request; -public class SpnegoUserStoreAuthorizationService extends UserStoreAuthorizationService { +public class SpnegoUserStoreRoleProvider extends UserStoreRoleProvider { - public SpnegoUserStoreAuthorizationService(String privilegesFilePath) { + public SpnegoUserStoreRoleProvider(String privilegesFilePath) { super(privilegesFilePath); } - public SpnegoUserStoreAuthorizationService(UserStore userStore) { + public SpnegoUserStoreRoleProvider(UserStore userStore) { super(userStore); } @Override - public UserIdentity getUserIdentity(HttpServletRequest request, String name) { + public String[] rolesFor(Request request, String name) { int hostSeparator = name.indexOf('/'); String shortName = hostSeparator > 0 ? name.substring(0, hostSeparator) : name; int realmSeparator = shortName.indexOf('@'); shortName = realmSeparator > 0 ? shortName.substring(0, realmSeparator) : shortName; - return super.getUserIdentity(request, shortName); + return super.rolesFor(request, shortName); } } diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyAuthorizationService.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyAuthorizationService.java deleted file mode 100644 index 917870c338..0000000000 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyAuthorizationService.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2020 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information. - */ - -package com.linkedin.kafka.cruisecontrol.servlet.security.trustedproxy; - -import com.linkedin.kafka.cruisecontrol.servlet.security.DefaultRoleSecurityProvider; -import com.linkedin.kafka.cruisecontrol.servlet.security.SecurityUtils; -import org.eclipse.jetty.security.UserStore; -import org.eclipse.jetty.security.authentication.AuthorizationService; -import org.eclipse.jetty.server.UserIdentity; -import org.eclipse.jetty.util.component.AbstractLifeCycle; -import javax.servlet.http.HttpServletRequest; -import java.util.List; -import java.util.regex.Pattern; - -/** - * This authorization service simply checks the incoming user against a list of configured service names maintained in - * a {@link UserStore} and if specified, the remote IP address against a configured pattern. - */ -public class TrustedProxyAuthorizationService extends AbstractLifeCycle implements AuthorizationService { - - private final UserStore _serviceUserStore; - private final Pattern _trustedProxyIpPattern; - - TrustedProxyAuthorizationService(List userNames, String trustedProxyIpPattern) { - _serviceUserStore = new UserStore(); - userNames.forEach(u -> _serviceUserStore.addUser(u, SecurityUtils.NO_CREDENTIAL, new String[] { DefaultRoleSecurityProvider.ADMIN })); - if (trustedProxyIpPattern != null) { - _trustedProxyIpPattern = Pattern.compile(trustedProxyIpPattern); - } else { - _trustedProxyIpPattern = null; - } - } - - @Override - public UserIdentity getUserIdentity(HttpServletRequest request, String name) { - // ConfigurableSpnegoAuthenticator may pass names in servicename/host format but we only store the servicename - int nameHostSeparatorIndex = name.indexOf('/'); - String serviceName = nameHostSeparatorIndex > 0 ? name.substring(0, nameHostSeparatorIndex) : name; - UserIdentity serviceIdentity = _serviceUserStore.getUserIdentity(serviceName); - if (_trustedProxyIpPattern != null) { - return _trustedProxyIpPattern.matcher(request.getRemoteAddr()).matches() ? serviceIdentity : null; - } else { - return serviceIdentity; - } - } - - @Override - protected void doStart() throws Exception { - _serviceUserStore.start(); - super.doStart(); - } - - @Override - protected void doStop() throws Exception { - super.doStop(); - _serviceUserStore.stop(); - } -} diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyLoginService.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyLoginService.java index ebf39b18d9..f13f0e3164 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyLoginService.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyLoginService.java @@ -4,38 +4,39 @@ package com.linkedin.kafka.cruisecontrol.servlet.security.trustedproxy; +import com.linkedin.kafka.cruisecontrol.servlet.security.RoleProvider; import com.linkedin.kafka.cruisecontrol.servlet.security.spnego.SpnegoLoginServiceWithAuthServiceLifecycle; import org.eclipse.jetty.security.IdentityService; import org.eclipse.jetty.security.LoginService; -import org.eclipse.jetty.security.SpnegoUserIdentity; -import org.eclipse.jetty.security.SpnegoUserPrincipal; -import org.eclipse.jetty.security.authentication.AuthorizationService; -import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.security.RoleDelegateUserIdentity; +import org.eclipse.jetty.security.SPNEGOUserPrincipal; +import org.eclipse.jetty.security.UserIdentity; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Session; +import org.eclipse.jetty.util.Fields; import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.component.LifeCycle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.security.auth.Subject; -import javax.servlet.ServletRequest; -import javax.servlet.http.HttpServletRequest; import java.nio.file.Path; import java.security.Principal; import java.util.Collections; import java.util.List; +import java.util.function.Function; import static com.linkedin.kafka.cruisecontrol.servlet.parameters.ParameterUtils.DO_AS; /** * {@code TrustedProxyLoginService} is a special SPNEGO login service where we only allow a list of trusted services - * to act on behalf of clients. The login service authenticates the trusted party but creates credentials for the client - * based on the {@link AuthorizationService}. + * to act on behalf of clients. */ public class TrustedProxyLoginService extends ContainerLifeCycle implements LoginService { private static final Logger LOG = LoggerFactory.getLogger(TrustedProxyLoginService.class); public static final boolean READ_ONLY_SUBJECT = true; - // authorizes the end user that is passed in via the doAs header - private final AuthorizationService _endUserAuthorizer; + // authorizes the end user passed in via the doAs header + private final RoleProvider _roleProvider; // use encapsulation instead of inheritance as it's easier to test private final SpnegoLoginServiceWithAuthServiceLifecycle _delegateSpnegoLoginService; private final SpnegoLoginServiceWithAuthServiceLifecycle _fallbackSpnegoLoginService; @@ -44,29 +45,29 @@ public class TrustedProxyLoginService extends ContainerLifeCycle implements Logi /** * Creates a new instance based on the kerberos realm, the list of trusted proxies, their allowed IP pattern and the - * {@link AuthorizationService} that stores the end users. + * {@link RoleProvider} that stores the end users. * @param realm is the kerberos realm of the spnego service principal that is used by Cruise Control - * @param userAuthorizer authorizes the user that is passed in via the doAs header + * @param roleProvider authorizes the user passed in via the doAs header * @param trustedProxies is a list of kerberos service shortnames that identifies the trusted proxies * @param trustedProxyIpPattern is a Java regex pattern that defines which IP addresses can be accepted by * Cruise Control as trusted proxies */ - public TrustedProxyLoginService(String realm, AuthorizationService userAuthorizer, List trustedProxies, + public TrustedProxyLoginService(String realm, RoleProvider roleProvider, List trustedProxies, String trustedProxyIpPattern, boolean fallbackToSpnegoAllowed, List principalToLocalRules) { _delegateSpnegoLoginService = new SpnegoLoginServiceWithAuthServiceLifecycle( - realm, new TrustedProxyAuthorizationService(trustedProxies, trustedProxyIpPattern), principalToLocalRules); - _fallbackSpnegoLoginService = new SpnegoLoginServiceWithAuthServiceLifecycle(realm, userAuthorizer, principalToLocalRules); - _endUserAuthorizer = userAuthorizer; + realm, new TrustedProxyUserStoreRoleProvider(trustedProxies, trustedProxyIpPattern), principalToLocalRules); + _fallbackSpnegoLoginService = new SpnegoLoginServiceWithAuthServiceLifecycle(realm, roleProvider, principalToLocalRules); + _roleProvider = roleProvider; _fallbackToSpnegoAllowed = fallbackToSpnegoAllowed; } // visible for testing TrustedProxyLoginService(SpnegoLoginServiceWithAuthServiceLifecycle delegateSpnegoLoginService, SpnegoLoginServiceWithAuthServiceLifecycle fallbackSpnegoLoginService, - AuthorizationService userAuthorizer, boolean fallbackToSpnegoAllowed) { + RoleProvider roleProvider, boolean fallbackToSpnegoAllowed) { _delegateSpnegoLoginService = delegateSpnegoLoginService; _fallbackSpnegoLoginService = fallbackSpnegoLoginService; - _endUserAuthorizer = userAuthorizer; + _roleProvider = roleProvider; _fallbackToSpnegoAllowed = fallbackToSpnegoAllowed; } @@ -107,25 +108,31 @@ public String getName() { } @Override - public UserIdentity login(String username, Object credentials, ServletRequest request) { - if (!(request instanceof HttpServletRequest)) { - return null; - } - String doAsUser = request.getParameter(DO_AS); + public UserIdentity login(String username, Object credentials, Request request, Function getOrCreateSession) { + Fields reqParameters; + try { + reqParameters = Request.getParameters(request); + } catch (Exception e) { + throw new RuntimeException(e); + } + String doAsUser = reqParameters.getValue(DO_AS); if (doAsUser == null && _fallbackToSpnegoAllowed) { - SpnegoUserIdentity fallbackIdentity = (SpnegoUserIdentity) _fallbackSpnegoLoginService.login(username, credentials, request); + RoleDelegateUserIdentity fallbackIdentity = (RoleDelegateUserIdentity) _fallbackSpnegoLoginService.login(username, + credentials, request, getOrCreateSession); if (!fallbackIdentity.isEstablished()) { - SpnegoUserPrincipal fallbackPrincipal = (SpnegoUserPrincipal) fallbackIdentity.getUserPrincipal(); + SPNEGOUserPrincipal fallbackPrincipal = (SPNEGOUserPrincipal) fallbackIdentity.getUserPrincipal(); LOG.info("Service user {} isn't authorized as spnego fallback principal", fallbackPrincipal.getName()); } return fallbackIdentity; } else { - SpnegoUserIdentity serviceIdentity = (SpnegoUserIdentity) _delegateSpnegoLoginService.login(username, credentials, request); - SpnegoUserPrincipal servicePrincipal = (SpnegoUserPrincipal) serviceIdentity.getUserPrincipal(); + RoleDelegateUserIdentity serviceIdentity = (RoleDelegateUserIdentity) _delegateSpnegoLoginService.login(username, + credentials, request, getOrCreateSession); + SPNEGOUserPrincipal servicePrincipal = (SPNEGOUserPrincipal) serviceIdentity.getUserPrincipal(); LOG.info("Authorizing proxy user {} from {} service", doAsUser, servicePrincipal.getName()); UserIdentity doAsIdentity = null; if (doAsUser != null && !doAsUser.isEmpty()) { - doAsIdentity = _endUserAuthorizer.getUserIdentity((HttpServletRequest) request, doAsUser); + String[] roles = _roleProvider.rolesFor(request, doAsUser); + doAsIdentity = getIdentityService().newUserIdentity(serviceIdentity.getSubject(), servicePrincipal, roles); } Principal principal = new TrustedProxyPrincipal(doAsUser, servicePrincipal); @@ -133,12 +140,12 @@ public UserIdentity login(String username, Object credentials, ServletRequest re if (!serviceIdentity.isEstablished()) { LOG.info("Service user {} isn't authorized as a trusted proxy", servicePrincipal.getName()); - return new SpnegoUserIdentity(subject, principal, null); + return new RoleDelegateUserIdentity(subject, principal, null); } else { if (doAsIdentity == null) { LOG.info("Couldn't authorize user {}", doAsUser); } - return new SpnegoUserIdentity(subject, principal, doAsIdentity); + return new RoleDelegateUserIdentity(subject, principal, doAsIdentity); } } } @@ -167,8 +174,8 @@ public void logout(UserIdentity user) { @Override protected void doStart() throws Exception { - if (_endUserAuthorizer instanceof LifeCycle) { - ((LifeCycle) _endUserAuthorizer).start(); + if (_roleProvider instanceof LifeCycle) { + ((LifeCycle) _roleProvider).start(); } _delegateSpnegoLoginService.start(); _fallbackSpnegoLoginService.start(); @@ -180,8 +187,8 @@ protected void doStop() throws Exception { super.doStop(); _fallbackSpnegoLoginService.stop(); _delegateSpnegoLoginService.stop(); - if (_endUserAuthorizer instanceof LifeCycle) { - ((LifeCycle) _endUserAuthorizer).stop(); + if (_roleProvider instanceof LifeCycle) { + ((LifeCycle) _roleProvider).stop(); } } } diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyPrincipal.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyPrincipal.java index 43881ae90d..3228a67f55 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyPrincipal.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyPrincipal.java @@ -4,14 +4,14 @@ package com.linkedin.kafka.cruisecontrol.servlet.security.trustedproxy; -import org.eclipse.jetty.security.SpnegoUserPrincipal; +import org.eclipse.jetty.security.SPNEGOUserPrincipal; -public class TrustedProxyPrincipal extends SpnegoUserPrincipal { +public class TrustedProxyPrincipal extends SPNEGOUserPrincipal { - private final SpnegoUserPrincipal _serviceUserPrincipal; + private final SPNEGOUserPrincipal _serviceUserPrincipal; private final String _doAsPrincipal; - public TrustedProxyPrincipal(String doAsPrincipal, SpnegoUserPrincipal serviceUserPrincipal) { + public TrustedProxyPrincipal(String doAsPrincipal, SPNEGOUserPrincipal serviceUserPrincipal) { super(doAsPrincipal, serviceUserPrincipal.getEncodedToken()); _doAsPrincipal = doAsPrincipal; _serviceUserPrincipal = serviceUserPrincipal; @@ -22,7 +22,7 @@ public String getName() { return _doAsPrincipal; } - public SpnegoUserPrincipal servicePrincipal() { + public SPNEGOUserPrincipal servicePrincipal() { return _serviceUserPrincipal; } } diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxySecurityProvider.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxySecurityProvider.java index cd8ba8d6c3..0b52ef8175 100644 --- a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxySecurityProvider.java +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxySecurityProvider.java @@ -9,7 +9,7 @@ import com.linkedin.kafka.cruisecontrol.servlet.security.spnego.SpnegoSecurityProvider; import org.eclipse.jetty.security.Authenticator; import org.eclipse.jetty.security.LoginService; -import org.eclipse.jetty.security.authentication.ConfigurableSpnegoAuthenticator; +import org.eclipse.jetty.security.authentication.SPNEGOAuthenticator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.file.Paths; @@ -17,7 +17,7 @@ /** * In trusted proxy authentication Cruise Control has a fronting proxy which authenticates clients and from which - * Cruise Control accepts requests as authenticated ones. the authenticated user's ID is forwarded in the "doAs" HTTP + * Cruise Control accepts requests as authenticated ones. The authenticated user's ID is forwarded in the "doAs" HTTP * GET query parameter. */ public class TrustedProxySecurityProvider extends SpnegoSecurityProvider { @@ -45,7 +45,7 @@ public void init(KafkaCruiseControlConfig config) { @Override public LoginService loginService() { - TrustedProxyLoginService loginService = new TrustedProxyLoginService(_spnegoPrincipal.realm(), authorizationService(), + TrustedProxyLoginService loginService = new TrustedProxyLoginService(_spnegoPrincipal.realm(), roleProvider(), _trustedProxyServices, _trustedProxyServicesIpRegex, _fallbackToSpnegoAllowed, _spnegoPrincipalToLocalRules); loginService.setServiceName(_spnegoPrincipal.serviceName()); loginService.setHostName(_spnegoPrincipal.hostName()); @@ -55,6 +55,6 @@ public LoginService loginService() { @Override public Authenticator authenticator() { - return new ConfigurableSpnegoAuthenticator(); + return new SPNEGOAuthenticator(); } } diff --git a/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyUserStoreRoleProvider.java b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyUserStoreRoleProvider.java new file mode 100644 index 0000000000..437f935bf7 --- /dev/null +++ b/cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyUserStoreRoleProvider.java @@ -0,0 +1,62 @@ +/* + * Copyright 2025 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information. + */ + +package com.linkedin.kafka.cruisecontrol.servlet.security.trustedproxy; + +import com.linkedin.kafka.cruisecontrol.servlet.security.DefaultRoleSecurityProvider; +import com.linkedin.kafka.cruisecontrol.servlet.security.RoleProvider; +import com.linkedin.kafka.cruisecontrol.servlet.security.SecurityUtils; +import org.eclipse.jetty.security.RolePrincipal; +import org.eclipse.jetty.security.UserStore; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import java.util.List; +import java.util.regex.Pattern; + +public class TrustedProxyUserStoreRoleProvider extends AbstractLifeCycle implements RoleProvider { + private final UserStore _serviceUserStore; + private final Pattern _trustedProxyIpPattern; + + public TrustedProxyUserStoreRoleProvider(List userNames, String trustedProxyIpPattern) { + _serviceUserStore = new UserStore(); + userNames.forEach(u -> + _serviceUserStore.addUser(u, SecurityUtils.NO_CREDENTIAL, + new String[]{DefaultRoleSecurityProvider.ADMIN})); + this._trustedProxyIpPattern = trustedProxyIpPattern == null ? null : Pattern.compile(trustedProxyIpPattern); + } + + @Override + public String[] rolesFor(Request request, String name) { + int nameHostSeparatorIndex = name.indexOf('/'); + String serviceName = (nameHostSeparatorIndex > 0) ? name.substring(0, nameHostSeparatorIndex) : name; + + List rolePrincipals = _serviceUserStore.getRolePrincipals(serviceName); + if (rolePrincipals == null || rolePrincipals.isEmpty()) { + return null; + } + String[] roles = rolePrincipals.stream().map(RolePrincipal::getName).toArray(String[]::new); + + if (_trustedProxyIpPattern == null) { + return roles; + } + + String remoteAddr = Request.getRemoteAddr(request); + return _trustedProxyIpPattern.matcher(remoteAddr).matches() ? roles : null; + } + + @Override + protected void doStart() throws Exception { + _serviceUserStore.start(); + super.doStart(); + } + + @Override + protected void doStop() throws Exception { + try { + _serviceUserStore.stop(); + } finally { + super.doStop(); + } + } +} diff --git a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/monitor/sampling/prometheus/PrometheusAdapterTest.java b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/monitor/sampling/prometheus/PrometheusAdapterTest.java index c5780d8793..5dad0433aa 100644 --- a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/monitor/sampling/prometheus/PrometheusAdapterTest.java +++ b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/monitor/sampling/prometheus/PrometheusAdapterTest.java @@ -9,7 +9,7 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponse; import org.apache.http.HttpEntity; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; diff --git a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/KafkaCruiseControlServletEndpointTest.java b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/KafkaCruiseControlServletEndpointTest.java index 3f82f41726..811d7551bd 100644 --- a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/KafkaCruiseControlServletEndpointTest.java +++ b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/KafkaCruiseControlServletEndpointTest.java @@ -21,7 +21,7 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.function.Function; -import javax.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSession; import org.apache.kafka.common.utils.MockTime; import org.apache.kafka.common.utils.Time; import org.easymock.EasyMock; diff --git a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/UserPermissionsIntegrationTest.java b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/UserPermissionsIntegrationTest.java index 17d37f8d9e..c20703c088 100644 --- a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/UserPermissionsIntegrationTest.java +++ b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/UserPermissionsIntegrationTest.java @@ -14,7 +14,7 @@ import org.eclipse.jetty.http.HttpHeader; import org.junit.After; import org.junit.Test; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; diff --git a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/UserTaskManagerTest.java b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/UserTaskManagerTest.java index 25fff3579f..bdb5d526d9 100644 --- a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/UserTaskManagerTest.java +++ b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/UserTaskManagerTest.java @@ -12,9 +12,9 @@ import org.easymock.EasyMock; import org.junit.Assert; import org.junit.Test; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; import java.util.Collections; import java.util.HashMap; import java.util.Map; diff --git a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/AuthenticationIntegrationTest.java b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/AuthenticationIntegrationTest.java index 4ebf65dc2f..d951a30041 100644 --- a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/AuthenticationIntegrationTest.java +++ b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/AuthenticationIntegrationTest.java @@ -10,21 +10,21 @@ import org.apache.http.auth.BasicUserPrincipal; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.security.Authenticator; -import org.eclipse.jetty.security.ConstraintMapping; +import org.eclipse.jetty.ee10.servlet.security.ConstraintMapping; import org.eclipse.jetty.security.DefaultIdentityService; -import org.eclipse.jetty.security.DefaultUserIdentity; import org.eclipse.jetty.security.IdentityService; import org.eclipse.jetty.security.LoginService; +import org.eclipse.jetty.security.UserIdentity; import org.eclipse.jetty.security.authentication.BasicAuthenticator; -import org.eclipse.jetty.server.UserIdentity; -import org.eclipse.jetty.util.security.Constraint; +import org.eclipse.jetty.security.Constraint; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Session; import org.eclipse.jetty.util.security.Credential; import org.junit.After; import org.junit.Before; import org.junit.Test; import javax.security.auth.Subject; -import javax.servlet.ServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URI; @@ -36,6 +36,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; import static com.linkedin.kafka.cruisecontrol.servlet.CruiseControlEndPoint.STATE; import static org.junit.Assert.assertEquals; @@ -93,10 +94,8 @@ public void init(KafkaCruiseControlConfig config) { } @Override public List constraintMappings() { ConstraintMapping mapping = new ConstraintMapping(); - Constraint constraint = new Constraint(); - constraint.setAuthenticate(true); - constraint.setName(Constraint.__BASIC_AUTH); - constraint.setRoles(new String[] { ADMIN_ROLE }); + Constraint.Builder builder = new Constraint.Builder(); + Constraint constraint = builder.roles(new String[] { ADMIN_ROLE }).name("BASIC").authorization(Constraint.Authorization.SPECIFIC_ROLE).build(); mapping.setConstraint(constraint); mapping.setPathSpec(ANY_PATH); @@ -120,29 +119,37 @@ public Set roles() { private static class ConstantLoginService implements LoginService { - private static final UserIdentity USER_IDENTITY = new DefaultUserIdentity( - new Subject(true, - Collections.singleton(new BasicUserPrincipal(TEST_USER)), - Collections.emptySet(), - Collections.singleton(Credential.getCredential(TEST_PASSWORD))), - new BasicUserPrincipal(TEST_USER), - new String[] { ADMIN_ROLE }); + private static final BasicUserPrincipal PRINCIPAL = new BasicUserPrincipal(TEST_USER); + private static final Subject SUBJECT = new Subject( + true, + Collections.singleton(PRINCIPAL), + Collections.emptySet(), + Collections.singleton(Credential.getCredential(TEST_PASSWORD)) + ); + private static final String[] ROLES = new String[] { ADMIN_ROLE }; private IdentityService _identityService = new DefaultIdentityService(); + private UserIdentity buildUserIdentity() { + if (_identityService == null) { + throw new IllegalStateException("IdentityService must be configured"); + } + return _identityService.newUserIdentity(SUBJECT, PRINCIPAL, ROLES); + } + @Override public String getName() { return null; } @Override - public UserIdentity login(String username, Object credentials, ServletRequest request) { - return TEST_USER.equals(username) && TEST_PASSWORD.equals(credentials) ? USER_IDENTITY : null; + public UserIdentity login(String username, Object credentials, Request request, Function getOrCreateSession) { + return TEST_USER.equals(username) && TEST_PASSWORD.equals(credentials) ? buildUserIdentity() : null; } @Override public boolean validate(UserIdentity user) { - return USER_IDENTITY.equals(user); + return buildUserIdentity().equals(user); } @Override diff --git a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/BasicAuthenticationIntegrationTest.java b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/BasicAuthenticationIntegrationTest.java index 2c99274c0d..6d12b01bee 100644 --- a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/BasicAuthenticationIntegrationTest.java +++ b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/BasicAuthenticationIntegrationTest.java @@ -10,7 +10,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URI; diff --git a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/SslConnectionIntegrationTest.java b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/SslConnectionIntegrationTest.java index 04fae18c21..741512a017 100644 --- a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/SslConnectionIntegrationTest.java +++ b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/SslConnectionIntegrationTest.java @@ -14,7 +14,7 @@ import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponse; import java.net.HttpURLConnection; import java.net.URI; import java.net.URL; diff --git a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtAuthenticatorTest.java b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtAuthenticatorTest.java index c540b99e9b..8127bb2d15 100644 --- a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtAuthenticatorTest.java +++ b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtAuthenticatorTest.java @@ -4,22 +4,30 @@ package com.linkedin.kafka.cruisecontrol.servlet.security.jwt; import com.linkedin.kafka.cruisecontrol.servlet.security.SecurityUtils; -import com.linkedin.kafka.cruisecontrol.servlet.security.UserStoreAuthorizationService; +import com.linkedin.kafka.cruisecontrol.servlet.security.UserStoreRoleProvider; +import org.easymock.EasyMock; +import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.security.AuthenticationState; import org.eclipse.jetty.security.Authenticator; import org.eclipse.jetty.security.DefaultIdentityService; import org.eclipse.jetty.security.ServerAuthException; -import org.eclipse.jetty.security.UserAuthentication; import org.eclipse.jetty.security.UserStore; -import org.eclipse.jetty.server.Authentication; +import org.eclipse.jetty.security.authentication.LoginAuthenticator; import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; import org.junit.Test; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import org.junit.runner.RunWith; +import org.powermock.api.easymock.PowerMock; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; import java.io.IOException; +import java.util.List; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.expectLastCall; @@ -33,6 +41,9 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +@RunWith(PowerMockRunner.class) +@PowerMockIgnore("javax.security.*") +@PrepareForTest(Response.class) public class JwtAuthenticatorTest { private static final String TEST_USER = "testUser"; @@ -48,30 +59,35 @@ public class JwtAuthenticatorTest { @Test public void testParseTokenFromAuthHeader() { JwtAuthenticator authenticator = new JwtAuthenticator(TOKEN_PROVIDER, JWT_TOKEN); - HttpServletRequest request = mock(HttpServletRequest.class); - expect(request.getHeader(HttpHeader.AUTHORIZATION.asString())).andReturn(JwtAuthenticator.BEARER + " " + EXPECTED_TOKEN); - replay(request); + Request request = mock(Request.class); + HttpFields headers = mock(HttpFields.class); + expect(request.getHeaders()).andReturn(headers).anyTimes(); + expect(headers.get(HttpHeader.AUTHORIZATION.asString())).andReturn(JwtAuthenticator.BEARER + " " + EXPECTED_TOKEN); + replay(request, headers); String actualToken = authenticator.getJwtFromBearerAuthorization(request); - verify(request); + verify(request, headers); assertEquals(EXPECTED_TOKEN, actualToken); } @Test public void testParseTokenFromAuthHeaderNoBearer() { JwtAuthenticator authenticator = new JwtAuthenticator(TOKEN_PROVIDER, JWT_TOKEN); - HttpServletRequest request = mock(HttpServletRequest.class); - expect(request.getHeader(HttpHeader.AUTHORIZATION.asString())).andReturn(BASIC_SCHEME + " " + EXPECTED_TOKEN); - replay(request); + Request request = mock(Request.class); + HttpFields headers = mock(HttpFields.class); + expect(request.getHeaders()).andReturn(headers).anyTimes(); + expect(headers.get(HttpHeader.AUTHORIZATION.asString())).andReturn(BASIC_SCHEME + " " + EXPECTED_TOKEN); + replay(request, headers); String actualToken = authenticator.getJwtFromBearerAuthorization(request); - verify(request); + verify(request, headers); assertNull(actualToken); } @Test public void testParseTokenFromCookie() { JwtAuthenticator authenticator = new JwtAuthenticator(TOKEN_PROVIDER, JWT_TOKEN); - HttpServletRequest request = mock(HttpServletRequest.class); - expect(request.getCookies()).andReturn(new Cookie[] {new Cookie(JWT_TOKEN, EXPECTED_TOKEN)}); + Request request = mock(Request.class); + HttpCookie jwtCookie = HttpCookie.build(JWT_TOKEN, EXPECTED_TOKEN).build(); + expect(request.getAttribute(Request.COOKIE_ATTRIBUTE)).andReturn(List.of(jwtCookie)); replay(request); String actualToken = authenticator.getJwtFromCookie(request); verify(request); @@ -81,8 +97,9 @@ public void testParseTokenFromCookie() { @Test public void testParseTokenFromCookieNoJwtCookie() { JwtAuthenticator authenticator = new JwtAuthenticator(TOKEN_PROVIDER, JWT_TOKEN); - HttpServletRequest request = mock(HttpServletRequest.class); - expect(request.getCookies()).andReturn(new Cookie[] {new Cookie(RANDOM_COOKIE_NAME, "")}); + Request request = mock(Request.class); + HttpCookie jwtCookie = HttpCookie.build(RANDOM_COOKIE_NAME, "").build(); + expect(request.getAttribute(Request.COOKIE_ATTRIBUTE)).andReturn(List.of(jwtCookie)); replay(request); String actualToken = authenticator.getJwtFromCookie(request); verify(request); @@ -93,21 +110,26 @@ public void testParseTokenFromCookieNoJwtCookie() { public void testRedirect() throws IOException, ServerAuthException { JwtAuthenticator authenticator = new JwtAuthenticator(TOKEN_PROVIDER, JWT_TOKEN); - HttpServletRequest request = mock(HttpServletRequest.class); - expect(request.getMethod()).andReturn(HttpMethod.GET.asString()); - expect(request.getQueryString()).andReturn(null); - expect(request.getHeader(HttpHeader.AUTHORIZATION.asString())).andReturn(null); - expect(request.getCookies()).andReturn(new Cookie[] {}); - expect(request.getRequestURL()).andReturn(new StringBuffer(CRUISE_CONTROL_ENDPOINT)); - - HttpServletResponse response = mock(HttpServletResponse.class); - response.sendRedirect(TOKEN_PROVIDER.replace(JwtAuthenticator.REDIRECT_URL, CRUISE_CONTROL_ENDPOINT)); - expectLastCall().andVoid(); - - replay(request, response); - Authentication actualAuthentication = authenticator.validateRequest(request, response, true); - verify(request, response); - assertEquals(Authentication.SEND_CONTINUE, actualAuthentication); + Request request = mock(Request.class); + HttpFields headers = mock(HttpFields.class); + expect(request.getMethod()).andReturn(HttpMethod.GET.asString()).anyTimes(); + expect(request.getHeaders()).andReturn(headers).anyTimes(); + expect(headers.get(HttpHeader.AUTHORIZATION.asString())).andReturn(null); + expect(request.getAttribute(Request.COOKIE_ATTRIBUTE)).andReturn(List.of()); + HttpURI uri = HttpURI.build().scheme("http").host("cruisecontrol.mycompany.com").port(80).path("/state").asImmutable(); + expect(request.getHttpURI()).andReturn(uri).anyTimes(); + + Response response = mock(Response.class); + PowerMock.mockStatic(Response.class); + Response.sendRedirect(request, response, null, TOKEN_PROVIDER.replace(JwtAuthenticator.REDIRECT_URL, CRUISE_CONTROL_ENDPOINT)); + PowerMock.expectLastCall().andVoid(); + + EasyMock.replay(request, headers, response); + PowerMock.replay(Response.class); + AuthenticationState actualAuthentication = authenticator.validateRequest(request, response, null); + EasyMock.verify(request, headers, response); + PowerMock.verify(Response.class); + assertEquals(AuthenticationState.CHALLENGE, actualAuthentication); } @Test @@ -115,28 +137,32 @@ public void testSuccessfulLogin() throws Exception { UserStore testUserStore = new UserStore(); testUserStore.addUser(TEST_USER, SecurityUtils.NO_CREDENTIAL, new String[]{USER_ROLE}); TokenGenerator.TokenAndKeys tokenAndKeys = TokenGenerator.generateToken(TEST_USER); - JwtLoginService loginService = new JwtLoginService(new UserStoreAuthorizationService(testUserStore), tokenAndKeys.publicKey(), null); + JwtLoginService loginService = new JwtLoginService(new UserStoreRoleProvider(testUserStore), tokenAndKeys.publicKey(), null); - Authenticator.AuthConfiguration configuration = mock(Authenticator.AuthConfiguration.class); + Authenticator.Configuration configuration = mock(Authenticator.Configuration.class); expect(configuration.getLoginService()).andReturn(loginService); expect(configuration.getIdentityService()).andReturn(new DefaultIdentityService()); expect(configuration.isSessionRenewedOnAuthentication()).andReturn(true); + expect(configuration.getSessionMaxInactiveIntervalOnAuthentication()).andReturn(0); Request request = niceMock(Request.class); + HttpFields headers = mock(HttpFields.class); expect(request.getMethod()).andReturn(HttpMethod.GET.asString()); - expect(request.getHeader(HttpHeader.AUTHORIZATION.asString())).andReturn(null); - request.setAttribute(JwtAuthenticator.JWT_TOKEN_REQUEST_ATTRIBUTE, tokenAndKeys.token()); - expectLastCall().andVoid(); - expect(request.getCookies()).andReturn(new Cookie[] {new Cookie(JWT_TOKEN, tokenAndKeys.token())}); + expect(request.getHeaders()).andReturn(headers); + expect(headers.get(HttpHeader.AUTHORIZATION.asString())).andReturn(null); + expect(request.setAttribute(JwtAuthenticator.JWT_TOKEN_REQUEST_ATTRIBUTE, tokenAndKeys.token())).andReturn(null); + HttpCookie jwtCookie = HttpCookie.build(JWT_TOKEN, tokenAndKeys.token()).build(); + expect(request.getAttribute(Request.COOKIE_ATTRIBUTE)).andReturn(List.of(jwtCookie)).anyTimes(); expect(request.getAttribute(JwtAuthenticator.JWT_TOKEN_REQUEST_ATTRIBUTE)).andReturn(tokenAndKeys.token()); - HttpServletResponse response = mock(HttpServletResponse.class); + Response response = mock(Response.class); - replay(configuration, request, response); + replay(configuration, request, headers, response); JwtAuthenticator authenticator = new JwtAuthenticator(TOKEN_PROVIDER, JWT_TOKEN); authenticator.setConfiguration(configuration); - UserAuthentication authentication = (UserAuthentication) authenticator.validateRequest(request, response, true); - verify(configuration, request, response); + LoginAuthenticator.UserAuthenticationSucceeded authentication = (LoginAuthenticator.UserAuthenticationSucceeded) + authenticator.validateRequest(request, response, null); + verify(configuration, request, headers, response); assertNotNull(authentication); assertThat(authentication.getUserIdentity().getUserPrincipal(), instanceOf(JwtUserPrincipal.class)); @@ -150,33 +176,35 @@ public void testFailedLoginWithUserNotFound() throws Exception { UserStore testUserStore = new UserStore(); testUserStore.addUser(TEST_USER_2, SecurityUtils.NO_CREDENTIAL, new String[] {USER_ROLE}); TokenGenerator.TokenAndKeys tokenAndKeys = TokenGenerator.generateToken(TEST_USER); - JwtLoginService loginService = new JwtLoginService(new UserStoreAuthorizationService(testUserStore), tokenAndKeys.publicKey(), null); + JwtLoginService loginService = new JwtLoginService(new UserStoreRoleProvider(testUserStore), tokenAndKeys.publicKey(), null); - Authenticator.AuthConfiguration configuration = mock(Authenticator.AuthConfiguration.class); + Authenticator.Configuration configuration = mock(Authenticator.Configuration.class); expect(configuration.getLoginService()).andReturn(loginService); expect(configuration.getIdentityService()).andReturn(new DefaultIdentityService()); expect(configuration.isSessionRenewedOnAuthentication()).andReturn(true); + expect(configuration.getSessionMaxInactiveIntervalOnAuthentication()).andReturn(0); Request request = niceMock(Request.class); + HttpFields headers = mock(HttpFields.class); expect(request.getMethod()).andReturn(HttpMethod.GET.asString()); - expect(request.getHeader(HttpHeader.AUTHORIZATION.asString())).andReturn(null); - request.setAttribute(JwtAuthenticator.JWT_TOKEN_REQUEST_ATTRIBUTE, tokenAndKeys.token()); - expectLastCall().andVoid(); - expect(request.getCookies()).andReturn(new Cookie[] {new Cookie(JWT_TOKEN, tokenAndKeys.token())}); - expect(request.getAttribute(JwtAuthenticator.JWT_TOKEN_REQUEST_ATTRIBUTE)).andReturn(tokenAndKeys.token()); + expect(request.getHeaders()).andReturn(headers); + expect(headers.get(HttpHeader.AUTHORIZATION.asString())).andReturn(null); + expect(request.setAttribute(JwtAuthenticator.JWT_TOKEN_REQUEST_ATTRIBUTE, tokenAndKeys.token())).andReturn(null); + HttpCookie jwtCookie = HttpCookie.build(JWT_TOKEN, tokenAndKeys.token()).build(); + expect(request.getAttribute(Request.COOKIE_ATTRIBUTE)).andReturn(List.of(jwtCookie)).anyTimes(); - HttpServletResponse response = mock(HttpServletResponse.class); + Response response = mock(Response.class); response.setStatus(HttpStatus.UNAUTHORIZED_401); expectLastCall().andVoid(); - replay(configuration, request, response); + replay(configuration, request, headers, response); JwtAuthenticator authenticator = new JwtAuthenticator(TOKEN_PROVIDER, JWT_TOKEN); authenticator.setConfiguration(configuration); - Authentication authentication = authenticator.validateRequest(request, response, true); - verify(configuration, request, response); + AuthenticationState authentication = authenticator.validateRequest(request, response, null); + verify(configuration, request, headers, response); assertNotNull(authentication); - assertEquals(Authentication.SEND_FAILURE, authentication); + assertEquals(AuthenticationState.SEND_FAILURE, authentication); } @Test @@ -185,31 +213,34 @@ public void testFailedLoginWithInvalidToken() throws Exception { testUserStore.addUser(TEST_USER_2, SecurityUtils.NO_CREDENTIAL, new String[] {USER_ROLE}); TokenGenerator.TokenAndKeys tokenAndKeys = TokenGenerator.generateToken(TEST_USER); TokenGenerator.TokenAndKeys tokenAndKeys2 = TokenGenerator.generateToken(TEST_USER); - JwtLoginService loginService = new JwtLoginService(new UserStoreAuthorizationService(testUserStore), tokenAndKeys.publicKey(), null); + JwtLoginService loginService = new JwtLoginService(new UserStoreRoleProvider(testUserStore), tokenAndKeys.publicKey(), null); - Authenticator.AuthConfiguration configuration = mock(Authenticator.AuthConfiguration.class); + Authenticator.Configuration configuration = mock(Authenticator.Configuration.class); expect(configuration.getLoginService()).andReturn(loginService); expect(configuration.getIdentityService()).andReturn(new DefaultIdentityService()); expect(configuration.isSessionRenewedOnAuthentication()).andReturn(true); + expect(configuration.getSessionMaxInactiveIntervalOnAuthentication()).andReturn(0); Request request = niceMock(Request.class); + HttpFields headers = mock(HttpFields.class); expect(request.getMethod()).andReturn(HttpMethod.GET.asString()); - expect(request.getHeader(HttpHeader.AUTHORIZATION.asString())).andReturn(null); - request.setAttribute(JwtAuthenticator.JWT_TOKEN_REQUEST_ATTRIBUTE, tokenAndKeys2.token()); - expectLastCall().andVoid(); - expect(request.getCookies()).andReturn(new Cookie[] {new Cookie(JWT_TOKEN, tokenAndKeys2.token())}); + expect(request.getHeaders()).andReturn(headers); + expect(headers.get(HttpHeader.AUTHORIZATION.asString())).andReturn(null); + expect(request.setAttribute(JwtAuthenticator.JWT_TOKEN_REQUEST_ATTRIBUTE, tokenAndKeys2.token())).andReturn(null); + HttpCookie jwtCookie = HttpCookie.build(JWT_TOKEN, tokenAndKeys2.token()).build(); + expect(request.getAttribute(Request.COOKIE_ATTRIBUTE)).andReturn(List.of(jwtCookie)).anyTimes(); - HttpServletResponse response = mock(HttpServletResponse.class); + Response response = mock(Response.class); response.setStatus(HttpStatus.UNAUTHORIZED_401); expectLastCall().andVoid(); - replay(configuration, request, response); + replay(configuration, request, headers, response); JwtAuthenticator authenticator = new JwtAuthenticator(TOKEN_PROVIDER, JWT_TOKEN); authenticator.setConfiguration(configuration); - Authentication authentication = authenticator.validateRequest(request, response, true); - verify(configuration, request, response); + AuthenticationState authentication = authenticator.validateRequest(request, response, null); + verify(configuration, request, headers, response); assertNotNull(authentication); - assertEquals(Authentication.SEND_FAILURE, authentication); + assertEquals(AuthenticationState.SEND_FAILURE, authentication); } } diff --git a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtLoginServiceTest.java b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtLoginServiceTest.java index 25441b7f37..fd1fd0229e 100644 --- a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtLoginServiceTest.java +++ b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtLoginServiceTest.java @@ -5,12 +5,12 @@ package com.linkedin.kafka.cruisecontrol.servlet.security.jwt; import com.linkedin.kafka.cruisecontrol.servlet.security.SecurityUtils; -import com.linkedin.kafka.cruisecontrol.servlet.security.UserStoreAuthorizationService; +import com.linkedin.kafka.cruisecontrol.servlet.security.UserStoreRoleProvider; import com.nimbusds.jwt.SignedJWT; +import org.eclipse.jetty.security.UserIdentity; import org.eclipse.jetty.security.UserStore; -import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.server.Request; import org.junit.Test; -import javax.servlet.http.HttpServletRequest; import java.time.Clock; import java.time.Duration; import java.time.Instant; @@ -36,14 +36,14 @@ public void testValidateTokenSuccessfully() throws Exception { UserStore testUserStore = new UserStore(); testUserStore.addUser(TEST_USER, SecurityUtils.NO_CREDENTIAL, new String[] {"USER"}); TokenGenerator.TokenAndKeys tokenAndKeys = TokenGenerator.generateToken(TEST_USER); - JwtLoginService loginService = new JwtLoginService(new UserStoreAuthorizationService(testUserStore), tokenAndKeys.publicKey(), null); + JwtLoginService loginService = new JwtLoginService(new UserStoreRoleProvider(testUserStore), tokenAndKeys.publicKey(), null); SignedJWT jwtToken = SignedJWT.parse(tokenAndKeys.token()); - HttpServletRequest request = mock(HttpServletRequest.class); + Request request = mock(Request.class); expect(request.getAttribute(JwtAuthenticator.JWT_TOKEN_REQUEST_ATTRIBUTE)).andReturn(tokenAndKeys.token()); replay(request); - UserIdentity identity = loginService.login(TEST_USER, jwtToken, request); + UserIdentity identity = loginService.login(TEST_USER, jwtToken, request, null); verify(request); assertNotNull(identity); assertEquals(TEST_USER, identity.getUserPrincipal().getName()); @@ -56,12 +56,12 @@ public void testFailSignatureValidation() throws Exception { TokenGenerator.TokenAndKeys tokenAndKeys = TokenGenerator.generateToken(TEST_USER); // This will be signed with a different key TokenGenerator.TokenAndKeys tokenAndKeys2 = TokenGenerator.generateToken(TEST_USER); - JwtLoginService loginService = new JwtLoginService(new UserStoreAuthorizationService(testUserStore), tokenAndKeys2.publicKey(), null); + JwtLoginService loginService = new JwtLoginService(new UserStoreRoleProvider(testUserStore), tokenAndKeys2.publicKey(), null); SignedJWT jwtToken = SignedJWT.parse(tokenAndKeys.token()); - HttpServletRequest request = mock(HttpServletRequest.class); + Request request = mock(Request.class); - UserIdentity identity = loginService.login(TEST_USER, jwtToken, request); + UserIdentity identity = loginService.login(TEST_USER, jwtToken, request, null); assertNull(identity); } @@ -71,12 +71,12 @@ public void testFailAudienceValidation() throws Exception { testUserStore.addUser(TEST_USER, SecurityUtils.NO_CREDENTIAL, new String[] {"USER"}); TokenGenerator.TokenAndKeys tokenAndKeys = TokenGenerator.generateToken(TEST_USER, Arrays.asList("A", "B")); JwtLoginService loginService = new JwtLoginService( - new UserStoreAuthorizationService(testUserStore), tokenAndKeys.publicKey(), Arrays.asList("C", "D")); + new UserStoreRoleProvider(testUserStore), tokenAndKeys.publicKey(), Arrays.asList("C", "D")); SignedJWT jwtToken = SignedJWT.parse(tokenAndKeys.token()); - HttpServletRequest request = mock(HttpServletRequest.class); + Request request = mock(Request.class); - UserIdentity identity = loginService.login(TEST_USER, jwtToken, request); + UserIdentity identity = loginService.login(TEST_USER, jwtToken, request, null); assertNull(identity); } @@ -85,12 +85,12 @@ public void testFailExpirationValidation() throws Exception { UserStore testUserStore = new UserStore(); testUserStore.addUser(TEST_USER, SecurityUtils.NO_CREDENTIAL, new String[] {"USER"}); TokenGenerator.TokenAndKeys tokenAndKeys = TokenGenerator.generateToken(TEST_USER, 1L); - JwtLoginService loginService = new JwtLoginService(new UserStoreAuthorizationService(testUserStore), tokenAndKeys.publicKey(), null); + JwtLoginService loginService = new JwtLoginService(new UserStoreRoleProvider(testUserStore), tokenAndKeys.publicKey(), null); SignedJWT jwtToken = SignedJWT.parse(tokenAndKeys.token()); - HttpServletRequest request = mock(HttpServletRequest.class); + Request request = mock(Request.class); - UserIdentity identity = loginService.login(TEST_USER, jwtToken, request); + UserIdentity identity = loginService.login(TEST_USER, jwtToken, request, null); assertNull(identity); } @@ -99,14 +99,14 @@ public void testRevalidateTokenPasses() throws Exception { UserStore testUserStore = new UserStore(); testUserStore.addUser(TEST_USER, SecurityUtils.NO_CREDENTIAL, new String[] {"USER"}); TokenGenerator.TokenAndKeys tokenAndKeys = TokenGenerator.generateToken(TEST_USER); - JwtLoginService loginService = new JwtLoginService(new UserStoreAuthorizationService(testUserStore), tokenAndKeys.publicKey(), null); + JwtLoginService loginService = new JwtLoginService(new UserStoreRoleProvider(testUserStore), tokenAndKeys.publicKey(), null); SignedJWT jwtToken = SignedJWT.parse(tokenAndKeys.token()); - HttpServletRequest request = mock(HttpServletRequest.class); + Request request = mock(Request.class); expect(request.getAttribute(JwtAuthenticator.JWT_TOKEN_REQUEST_ATTRIBUTE)).andReturn(tokenAndKeys.token()); replay(request); - UserIdentity identity = loginService.login(TEST_USER, jwtToken, request); + UserIdentity identity = loginService.login(TEST_USER, jwtToken, request, null); verify(request); assertNotNull(identity); assertEquals(TEST_USER, identity.getUserPrincipal().getName()); @@ -121,14 +121,14 @@ public void testRevalidateTokenFails() throws Exception { TokenGenerator.TokenAndKeys tokenAndKeys = TokenGenerator.generateToken(TEST_USER, now.plusSeconds(10).toEpochMilli()); Clock fixedClock = Clock.fixed(now, ZoneOffset.UTC); JwtLoginService loginService = new JwtLoginService( - new UserStoreAuthorizationService(testUserStore), tokenAndKeys.publicKey(), null, fixedClock); + new UserStoreRoleProvider(testUserStore), tokenAndKeys.publicKey(), null, fixedClock); SignedJWT jwtToken = SignedJWT.parse(tokenAndKeys.token()); - HttpServletRequest request = mock(HttpServletRequest.class); + Request request = mock(Request.class); expect(request.getAttribute(JwtAuthenticator.JWT_TOKEN_REQUEST_ATTRIBUTE)).andReturn(tokenAndKeys.token()); replay(request); - UserIdentity identity = loginService.login(TEST_USER, jwtToken, request); + UserIdentity identity = loginService.login(TEST_USER, jwtToken, request, null); verify(request); assertNotNull(identity); assertEquals(TEST_USER, identity.getUserPrincipal().getName()); diff --git a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtSecurityProviderIntegrationTest.java b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtSecurityProviderIntegrationTest.java index 34d6959b7a..562359b80e 100644 --- a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtSecurityProviderIntegrationTest.java +++ b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtSecurityProviderIntegrationTest.java @@ -6,31 +6,35 @@ import com.linkedin.kafka.cruisecontrol.CruiseControlIntegrationTestHarness; import com.linkedin.kafka.cruisecontrol.config.constants.WebServerConfig; +import jakarta.servlet.http.HttpServletResponse; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.security.HashLoginService; +import org.eclipse.jetty.security.UserIdentity; +import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.UserIdentity; -import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.server.handler.ResourceHandler; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.Fields; +import org.eclipse.jetty.util.resource.ResourceFactory; import org.junit.After; import org.junit.Before; import org.junit.Test; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.FileOutputStream; -import java.io.IOException; import java.io.OutputStream; import java.math.BigInteger; import java.net.HttpURLConnection; import java.net.URI; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.Provider; import java.security.Security; @@ -61,30 +65,35 @@ public class JwtSecurityProviderIntegrationTest extends CruiseControlIntegration private final Server _tokenProviderServer; private final File _publicKeyFile; - class TestAuthenticatorHandler extends AbstractHandler { + class TestAuthenticatorHandler extends Handler.Abstract { private final HashLoginService _loginService; TestAuthenticatorHandler() { _loginService = new HashLoginService(); - _loginService.setConfig( - Objects.requireNonNull(this.getClass().getClassLoader().getResource(BASIC_AUTH_CREDENTIALS_FILE)).getPath()); + URL resourceUrl = Objects.requireNonNull(this.getClass().getClassLoader().getResource(BASIC_AUTH_CREDENTIALS_FILE)); + ResourceHandler rh = new ResourceHandler(); + _loginService.setConfig(ResourceFactory.of(rh).newResource(resourceUrl)); } @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { - String username = request.getParameter(TEST_USERNAME_KEY); - String password = request.getParameter(TEST_PASSWORD_KEY); + public boolean handle(Request request, Response response, Callback callback) throws Exception { + Fields params = Request.getParameters(request); + String username = params.getValue(TEST_USERNAME_KEY); + String password = params.getValue(TEST_PASSWORD_KEY); - String cruiseControlUrl = request.getParameter(ORIGIN); + String cruiseControlUrl = params.getValue(ORIGIN); System.out.println(String.format("Handling login: %s %s %s", username, password, cruiseControlUrl)); - UserIdentity identity = _loginService.login(username, password, request); + UserIdentity identity = _loginService.login(username, password, request, null); if (identity != null) { - response.addCookie(new Cookie(JWT_TOKEN_COOKIE_NAME, _tokenAndKeys.token())); + Response.addCookie(response, HttpCookie.from(JWT_TOKEN_COOKIE_NAME, _tokenAndKeys.token())); + response.setStatus(HttpServletResponse.SC_OK); + callback.succeeded(); } else { - response.sendError(HttpServletResponse.SC_FORBIDDEN); + Response.writeError(request, response, callback, HttpServletResponse.SC_FORBIDDEN); } + return true; } @Override diff --git a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoLoginServiceWithAuthServiceLifecycleTest.java b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoLoginServiceWithAuthServiceLifecycleTest.java index 736ac66599..d4883df630 100644 --- a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoLoginServiceWithAuthServiceLifecycleTest.java +++ b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoLoginServiceWithAuthServiceLifecycleTest.java @@ -4,13 +4,15 @@ package com.linkedin.kafka.cruisecontrol.servlet.security.spnego; +import com.linkedin.kafka.cruisecontrol.servlet.security.RoleProvider; +import com.linkedin.kafka.cruisecontrol.servlet.security.UserStoreRoleProvider; import org.apache.kafka.common.security.kerberos.KerberosShortNamer; -import org.eclipse.jetty.security.ConfigurableSpnegoLoginService; -import org.eclipse.jetty.security.SpnegoUserIdentity; -import org.eclipse.jetty.security.SpnegoUserPrincipal; -import org.eclipse.jetty.security.authentication.AuthorizationService; -import org.eclipse.jetty.server.UserIdentity; -import org.eclipse.jetty.server.UserIdentity.Scope; +import org.eclipse.jetty.security.IdentityService; +import org.eclipse.jetty.security.SPNEGOLoginService; +import org.eclipse.jetty.security.SPNEGOUserPrincipal; +import org.eclipse.jetty.security.UserIdentity; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Session; import org.ietf.jgss.GSSContext; import org.ietf.jgss.GSSException; import org.junit.Before; @@ -21,11 +23,11 @@ import org.powermock.modules.junit4.PowerMockRunner; import org.powermock.reflect.Whitebox; import javax.security.auth.Subject; -import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.Collections; import java.util.List; +import java.util.function.Function; import static org.easymock.EasyMock.anyObject; import static org.easymock.EasyMock.anyString; @@ -35,7 +37,8 @@ import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.verify; import static org.junit.Assert.assertEquals; -import static org.powermock.api.support.Stubber.stubMethod; +import static org.powermock.api.support.membermodification.MemberMatcher.method; +import static org.powermock.api.support.membermodification.MemberModifier.stub; /** * Unit tests for {@link SpnegoLoginServiceWithAuthServiceLifecycle} @@ -50,12 +53,12 @@ public class SpnegoLoginServiceWithAuthServiceLifecycleTest { private static final String ROLE = "ADMIN"; private static final Subject SUBJECT = new Subject(); private static final List ATL_RULES = Collections.singletonList("RULE:[1:$1@$0](.*@.*)s/@.*/foo/"); - private final AuthorizationService _mockAuthorizationService = mock(AuthorizationService.class); - private final ConfigurableSpnegoLoginService _mockLoginService = mock(ConfigurableSpnegoLoginService.class); - private final HttpServletRequest _mockRequest = mock(HttpServletRequest.class); - private final SpnegoUserIdentity _mockAuthIdentity = mock(SpnegoUserIdentity.class); - private final UserIdentity _mockRoleIdentity = mock(UserIdentity.class); - private final Scope _mockScope = mock(Scope.class); + private final RoleProvider _mockRoleProvider = mock(UserStoreRoleProvider.class); + private final SPNEGOLoginService _mockLoginService = mock(SPNEGOLoginService.class); + private final Request _mockRequest = mock(Request.class); + private final UserIdentity _mockAuthIdentity = mock(UserIdentity.class); + private final Function _mockGetOrCreateSession = mock(Function.class); + private final IdentityService _mockIdentityService = mock(IdentityService.class); private final GSSContext _mockGSSContext = mock(GSSContext.class); /** @@ -63,20 +66,19 @@ public class SpnegoLoginServiceWithAuthServiceLifecycleTest { */ @Before public void setup() throws GSSException { - expect(_mockLoginService.login(anyString(), anyObject(), anyObject())).andReturn(_mockAuthIdentity); - expect(_mockAuthIdentity.getSubject()).andReturn(SUBJECT); - expect(_mockRoleIdentity.isUserInRole(ROLE, _mockScope)).andReturn(true); + expect(_mockLoginService.login(anyString(), anyObject(), anyObject(), anyObject())).andReturn(_mockAuthIdentity); + expect(_mockAuthIdentity.getSubject()).andReturn(SUBJECT).anyTimes(); } @Test public void testExtractSpnegoContext() throws ReflectiveOperationException { SpnegoLoginServiceWithAuthServiceLifecycle service = partialMockBuilder(SpnegoLoginServiceWithAuthServiceLifecycle.class).createMock(); Whitebox.setInternalState(service, "_spnegoLoginService", _mockLoginService); - Class contextClass = Class.forName("org.eclipse.jetty.security.ConfigurableSpnegoLoginService$SpnegoContext"); + Class contextClass = Class.forName("org.eclipse.jetty.security.SPNEGOLoginService$SPNEGOContext"); Constructor contextCtor = contextClass.getDeclaredConstructor(); contextCtor.setAccessible(true); Object context = contextCtor.newInstance(); - Field contextField = ConfigurableSpnegoLoginService.class.getDeclaredField("_context"); + Field contextField = SPNEGOLoginService.class.getDeclaredField("_context"); contextField.setAccessible(true); contextField.set(_mockLoginService, context); replay(service); @@ -86,10 +88,10 @@ public void testExtractSpnegoContext() throws ReflectiveOperationException { @Test public void testLoginWithoutKerberosRules() { - SpnegoLoginServiceWithAuthServiceLifecycle service = createAuthServiceWithMocking(new SpnegoUserPrincipal(USERNAME, TOKEN)); - replay(service, _mockLoginService, _mockAuthorizationService, _mockAuthIdentity, _mockRoleIdentity); + SpnegoLoginServiceWithAuthServiceLifecycle service = createAuthServiceWithMocking(new SPNEGOUserPrincipal(USERNAME, TOKEN)); + replay(_mockLoginService, _mockRoleProvider, _mockAuthIdentity, _mockIdentityService); - UserIdentity userIdentity = service.login(USERNAME, new Object(), _mockRequest); + UserIdentity userIdentity = service.login(USERNAME, new Object(), _mockRequest, _mockGetOrCreateSession); assertUserIdentity(USERNAME, userIdentity); } @@ -98,30 +100,38 @@ public void testLoginWithoutKerberosRules() { public void testLoginWithKerberosRules() { String principalName = "user1@realm"; String usernameReplaced = USERNAME + "foo"; - SpnegoUserPrincipal principal = new SpnegoUserPrincipal(principalName, TOKEN); + SPNEGOUserPrincipal principal = new SPNEGOUserPrincipal(principalName, TOKEN); SpnegoLoginServiceWithAuthServiceLifecycle service = createAuthServiceWithMocking(principalName, usernameReplaced, principal); Whitebox.setInternalState(service, "_kerberosShortNamer", KerberosShortNamer.fromUnparsedRules(REALM, ATL_RULES)); - replay(service, _mockLoginService, _mockAuthorizationService, _mockAuthIdentity, _mockRoleIdentity); + replay(_mockLoginService, _mockRoleProvider, _mockAuthIdentity, _mockIdentityService); - UserIdentity userIdentity = service.login(principalName, new Object(), _mockRequest); + UserIdentity userIdentity = service.login(principalName, new Object(), _mockRequest, _mockGetOrCreateSession); assertUserIdentity(usernameReplaced, userIdentity); } - private SpnegoLoginServiceWithAuthServiceLifecycle createAuthServiceWithMocking(SpnegoUserPrincipal principal) { + private SpnegoLoginServiceWithAuthServiceLifecycle createAuthServiceWithMocking(SPNEGOUserPrincipal principal) { return createAuthServiceWithMocking(USERNAME, USERNAME, principal); } - private SpnegoLoginServiceWithAuthServiceLifecycle createAuthServiceWithMocking(String name, String finalName, SpnegoUserPrincipal principal) { - SpnegoLoginServiceWithAuthServiceLifecycle service = partialMockBuilder(SpnegoLoginServiceWithAuthServiceLifecycle.class).createMock(); - stubMethod(SpnegoLoginServiceWithAuthServiceLifecycle.class, "getFullPrincipalFromGssContext", name); - stubMethod(SpnegoLoginServiceWithAuthServiceLifecycle.class, "addContext", _mockGSSContext); + private SpnegoLoginServiceWithAuthServiceLifecycle createAuthServiceWithMocking(String name, String finalName, SPNEGOUserPrincipal principal) { + SpnegoLoginServiceWithAuthServiceLifecycle service = new SpnegoLoginServiceWithAuthServiceLifecycle(REALM, _mockRoleProvider, null); + stub(method(SpnegoLoginServiceWithAuthServiceLifecycle.class, "addContext", Request.class)).toReturn(_mockGSSContext); + stub(method(SpnegoLoginServiceWithAuthServiceLifecycle.class, "getFullPrincipalFromGssContext", GSSContext.class)).toReturn(name); - Whitebox.setInternalState(service, "_authorizationService", _mockAuthorizationService); Whitebox.setInternalState(service, "_spnegoLoginService", _mockLoginService); - expect(_mockAuthIdentity.getUserPrincipal()).andReturn(principal); - expect(_mockAuthorizationService.getUserIdentity(_mockRequest, finalName)).andReturn(_mockRoleIdentity); + expect(_mockAuthIdentity.getUserPrincipal()).andReturn(principal).anyTimes(); + expect(_mockRoleProvider.rolesFor(anyObject(), anyString())).andReturn(new String[]{ROLE}).anyTimes(); + expect(_mockLoginService.getIdentityService()).andReturn(_mockIdentityService).anyTimes(); + expect(_mockIdentityService.newUserIdentity(anyObject(), anyObject(), anyObject())).andReturn( + new org.eclipse.jetty.security.DefaultIdentityService() + .newUserIdentity( + SUBJECT, + new SPNEGOUserPrincipal(finalName, principal.getEncodedToken()), + new String[]{ROLE} + ) + ); return service; } @@ -129,8 +139,8 @@ private SpnegoLoginServiceWithAuthServiceLifecycle createAuthServiceWithMocking( private void assertUserIdentity(String username, UserIdentity userIdentity) { assertEquals(username, userIdentity.getUserPrincipal().getName()); assertEquals(SUBJECT, userIdentity.getSubject()); - userIdentity.isUserInRole(ROLE, _mockScope); - verify(_mockLoginService, _mockAuthorizationService, _mockRoleIdentity); + userIdentity.isUserInRole(ROLE); + verify(_mockLoginService, _mockRoleProvider, _mockAuthIdentity, _mockIdentityService); } } diff --git a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoSecurityProviderTestUtils.java b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoSecurityProviderTestUtils.java index 8a726bda9f..a59db36a12 100644 --- a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoSecurityProviderTestUtils.java +++ b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoSecurityProviderTestUtils.java @@ -6,15 +6,14 @@ import com.linkedin.kafka.cruisecontrol.KafkaCruiseControlApp; import com.linkedin.kafka.cruisecontrol.servlet.security.MiniKdc; +import jakarta.servlet.http.HttpServletResponse; import javax.security.auth.Subject; -import javax.servlet.http.HttpServletResponse; import java.net.HttpURLConnection; import java.net.URI; import java.security.PrivilegedAction; import static com.linkedin.kafka.cruisecontrol.servlet.CruiseControlEndPoint.STATE; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; /** * A test util class. @@ -48,13 +47,10 @@ public static void testNotAdminServiceLogin(MiniKdc miniKdc, KafkaCruiseControlA try { stateEndpointConnection = (HttpURLConnection) new URI(app.serverUrl()) .resolve(CRUISE_CONTROL_STATE_ENDPOINT).toURL().openConnection(); + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, stateEndpointConnection.getResponseCode()); } catch (Exception e) { throw new RuntimeException(e); } - // There is a bug in the Jetty implementation, and it doesn't seem to handle the connection - // properly in case of an error, so it somehow doesn't send a response code. To work this around - // I catch the RuntimeException that it throws. - assertThrows(RuntimeException.class, stateEndpointConnection::getResponseCode); return null; }); } diff --git a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoUserStoreAuthorizationServiceTest.java b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoUserStoreAuthorizationServiceTest.java deleted file mode 100644 index 518989f87d..0000000000 --- a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoUserStoreAuthorizationServiceTest.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2020 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information. - */ - -package com.linkedin.kafka.cruisecontrol.servlet.security.spnego; - -import com.linkedin.kafka.cruisecontrol.servlet.security.DefaultRoleSecurityProvider; -import com.linkedin.kafka.cruisecontrol.servlet.security.SecurityUtils; -import com.linkedin.kafka.cruisecontrol.servlet.security.UserStoreAuthorizationService; -import org.eclipse.jetty.security.UserStore; -import org.eclipse.jetty.server.UserIdentity; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -public class SpnegoUserStoreAuthorizationServiceTest { - - private static final String TEST_USER = "testUser"; - - @Test - public void testPrincipalNames() { - UserStore users = new UserStore(); - users.addUser(TEST_USER, SecurityUtils.NO_CREDENTIAL, new String[] { DefaultRoleSecurityProvider.ADMIN }); - UserStoreAuthorizationService usas = new SpnegoUserStoreAuthorizationService(users); - - UserIdentity result = usas.getUserIdentity(null, TEST_USER + "/host@REALM"); - assertNotNull(result); - assertEquals(TEST_USER, result.getUserPrincipal().getName()); - - result = usas.getUserIdentity(null, TEST_USER + "@REALM"); - assertNotNull(result); - assertEquals(TEST_USER, result.getUserPrincipal().getName()); - - result = usas.getUserIdentity(null, TEST_USER + "/host"); - assertNotNull(result); - assertEquals(TEST_USER, result.getUserPrincipal().getName()); - - result = usas.getUserIdentity(null, TEST_USER); - assertNotNull(result); - assertEquals(TEST_USER, result.getUserPrincipal().getName()); - } -} diff --git a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoUserStoreRoleProviderTest.java b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoUserStoreRoleProviderTest.java new file mode 100644 index 0000000000..578c45ee66 --- /dev/null +++ b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/spnego/SpnegoUserStoreRoleProviderTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information. + */ + +package com.linkedin.kafka.cruisecontrol.servlet.security.spnego; + +import com.linkedin.kafka.cruisecontrol.servlet.security.DefaultRoleSecurityProvider; +import com.linkedin.kafka.cruisecontrol.servlet.security.SecurityUtils; +import com.linkedin.kafka.cruisecontrol.servlet.security.UserStoreRoleProvider; +import org.eclipse.jetty.security.RolePrincipal; +import org.eclipse.jetty.security.UserStore; +import org.junit.Test; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class SpnegoUserStoreRoleProviderTest { + + private static final String TEST_USER = "testUser"; + + static class CapturingUserStore extends UserStore { + private volatile String _lastLookup; + + public String lastLookup() { + return _lastLookup; + } + + @Override + public List getRolePrincipals(String userName) { + _lastLookup = userName; + return super.getRolePrincipals(userName); + } + } + + @Test + public void testPrincipalNames() { + CapturingUserStore users = new CapturingUserStore(); + users.addUser(TEST_USER, SecurityUtils.NO_CREDENTIAL, new String[] { DefaultRoleSecurityProvider.ADMIN }); + UserStoreRoleProvider usas = new SpnegoUserStoreRoleProvider(users); + + String[] result = usas.rolesFor(null, TEST_USER + "/host@REALM"); + assertNotNull(result); + assertEquals("ADMIN", result[0]); + assertEquals(TEST_USER, users.lastLookup()); + + result = usas.rolesFor(null, TEST_USER + "@REALM"); + assertNotNull(result); + assertEquals("ADMIN", result[0]); + assertEquals(TEST_USER, users.lastLookup()); + + result = usas.rolesFor(null, TEST_USER + "/host"); + assertNotNull(result); + assertEquals("ADMIN", result[0]); + assertEquals(TEST_USER, users.lastLookup()); + + result = usas.rolesFor(null, TEST_USER); + assertNotNull(result); + assertEquals("ADMIN", result[0]); + assertEquals(TEST_USER, users.lastLookup()); + } +} diff --git a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyAuthorizationServiceTest.java b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyAuthorizationServiceTest.java index 941ba99a2a..a97aea72f8 100644 --- a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyAuthorizationServiceTest.java +++ b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyAuthorizationServiceTest.java @@ -4,9 +4,11 @@ package com.linkedin.kafka.cruisecontrol.servlet.security.trustedproxy; -import org.eclipse.jetty.server.UserIdentity; +import com.linkedin.kafka.cruisecontrol.servlet.security.DefaultRoleSecurityProvider; +import org.eclipse.jetty.server.ConnectionMetaData; +import org.eclipse.jetty.server.Request; import org.junit.Test; -import javax.servlet.http.HttpServletRequest; +import java.net.InetSocketAddress; import java.util.Collections; import static org.easymock.EasyMock.expect; @@ -21,20 +23,23 @@ public class TrustedProxyAuthorizationServiceTest { private static final String AUTH_SERVICE_NAME = "authservice"; private static final String IP_FILTER = "192\\.168\\.\\d{1,3}\\.\\d{1,3}"; + private static final String ROLE = DefaultRoleSecurityProvider.ADMIN; @Test public void testSuccessfulLoginWithIpFiltering() throws Exception { - TrustedProxyAuthorizationService srv = new TrustedProxyAuthorizationService(Collections.singletonList(AUTH_SERVICE_NAME), IP_FILTER); - HttpServletRequest mockRequest = mock(HttpServletRequest.class); + TrustedProxyUserStoreRoleProvider srv = new TrustedProxyUserStoreRoleProvider(Collections.singletonList(AUTH_SERVICE_NAME), IP_FILTER); + Request mockRequest = mock(Request.class); + ConnectionMetaData mockConnectionMetaData = mock(ConnectionMetaData.class); - expect(mockRequest.getRemoteAddr()).andReturn("192.168.0.1"); - replay(mockRequest); + expect(mockRequest.getConnectionMetaData()).andReturn(mockConnectionMetaData); + expect(mockConnectionMetaData.getRemoteSocketAddress()).andReturn(new InetSocketAddress("192.168.0.1", 12345)); + replay(mockRequest, mockConnectionMetaData); srv.start(); try { - UserIdentity result = srv.getUserIdentity(mockRequest, AUTH_SERVICE_NAME); + String[] result = srv.rolesFor(mockRequest, AUTH_SERVICE_NAME); assertNotNull(result); - assertEquals(AUTH_SERVICE_NAME, result.getUserPrincipal().getName()); - verify(mockRequest); + assertEquals(new String[]{ROLE}, result); + verify(mockRequest, mockConnectionMetaData); } finally { srv.stop(); } @@ -42,16 +47,18 @@ public void testSuccessfulLoginWithIpFiltering() throws Exception { @Test public void testUnsuccessfulLoginWithIpFiltering() throws Exception { - TrustedProxyAuthorizationService srv = new TrustedProxyAuthorizationService(Collections.singletonList(AUTH_SERVICE_NAME), IP_FILTER); - HttpServletRequest mockRequest = mock(HttpServletRequest.class); + TrustedProxyUserStoreRoleProvider srv = new TrustedProxyUserStoreRoleProvider(Collections.singletonList(AUTH_SERVICE_NAME), IP_FILTER); + Request mockRequest = mock(Request.class); + ConnectionMetaData mockConnectionMetaData = mock(ConnectionMetaData.class); - expect(mockRequest.getRemoteAddr()).andReturn("192.167.0.1"); - replay(mockRequest); + expect(mockRequest.getConnectionMetaData()).andReturn(mockConnectionMetaData); + expect(mockConnectionMetaData.getRemoteSocketAddress()).andReturn(new InetSocketAddress("192.167.0.1", 12345)); + replay(mockRequest, mockConnectionMetaData); srv.start(); try { - UserIdentity result = srv.getUserIdentity(mockRequest, AUTH_SERVICE_NAME); + String[] result = srv.rolesFor(mockRequest, AUTH_SERVICE_NAME); assertNull(result); - verify(mockRequest); + verify(mockRequest, mockConnectionMetaData); } finally { srv.stop(); } @@ -59,14 +66,14 @@ public void testUnsuccessfulLoginWithIpFiltering() throws Exception { @Test public void testSuccessfulLoginWithoutIpFiltering() throws Exception { - TrustedProxyAuthorizationService srv = new TrustedProxyAuthorizationService(Collections.singletonList(AUTH_SERVICE_NAME), null); - HttpServletRequest mockRequest = mock(HttpServletRequest.class); + TrustedProxyUserStoreRoleProvider srv = new TrustedProxyUserStoreRoleProvider(Collections.singletonList(AUTH_SERVICE_NAME), null); + Request mockRequest = mock(Request.class); replay(mockRequest); srv.start(); try { - UserIdentity result = srv.getUserIdentity(mockRequest, AUTH_SERVICE_NAME); + String[] result = srv.rolesFor(mockRequest, AUTH_SERVICE_NAME); assertNotNull(result); - assertEquals(AUTH_SERVICE_NAME, result.getUserPrincipal().getName()); + assertEquals(new String[]{ROLE}, result); verify(mockRequest); } finally { srv.stop(); diff --git a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyLoginServiceTest.java b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyLoginServiceTest.java index 8db3ca8e67..bb5454d799 100644 --- a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyLoginServiceTest.java +++ b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxyLoginServiceTest.java @@ -5,17 +5,25 @@ package com.linkedin.kafka.cruisecontrol.servlet.security.trustedproxy; import com.linkedin.kafka.cruisecontrol.servlet.security.DefaultRoleSecurityProvider; +import com.linkedin.kafka.cruisecontrol.servlet.security.RoleProvider; import com.linkedin.kafka.cruisecontrol.servlet.security.SecurityUtils; import com.linkedin.kafka.cruisecontrol.servlet.security.spnego.SpnegoLoginServiceWithAuthServiceLifecycle; -import org.eclipse.jetty.security.SpnegoUserIdentity; -import org.eclipse.jetty.security.SpnegoUserPrincipal; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.security.IdentityService; +import org.eclipse.jetty.security.RoleDelegateUserIdentity; +import org.eclipse.jetty.security.RolePrincipal; +import org.eclipse.jetty.security.SPNEGOUserPrincipal; +import org.eclipse.jetty.security.UserIdentity; import org.eclipse.jetty.security.UserStore; -import org.eclipse.jetty.security.authentication.AuthorizationService; -import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.server.ConnectionMetaData; +import org.eclipse.jetty.server.Context; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.Request; import org.junit.Test; import javax.security.auth.Subject; -import javax.servlet.http.HttpServletRequest; import java.util.Collections; +import java.util.List; +import java.util.Set; import static com.linkedin.kafka.cruisecontrol.servlet.parameters.ParameterUtils.DO_AS; import static org.easymock.EasyMock.anyObject; @@ -36,7 +44,7 @@ public class TrustedProxyLoginServiceTest { public static final String ENCODED_TOKEN = "encoded_token"; public static final String TEST_USER = "testUser"; - private static class TestAuthorizer implements AuthorizationService { + private static class TestAuthorizer implements RoleProvider { private final UserStore _adminUserStore = new UserStore(); @@ -45,8 +53,12 @@ private static class TestAuthorizer implements AuthorizationService { } @Override - public UserIdentity getUserIdentity(HttpServletRequest request, String name) { - return _adminUserStore.getUserIdentity(name); + public String[] rolesFor(Request request, String name) { + List rolePrincipals = _adminUserStore.getRolePrincipals(name); + if (rolePrincipals == null || rolePrincipals.isEmpty()) { + return null; + } + return rolePrincipals.stream().map(RolePrincipal::getName).toArray(String[]::new); } } @@ -55,118 +67,184 @@ public UserIdentity getUserIdentity(HttpServletRequest request, String name) { @Test public void testSuccessfulAuthentication() { - SpnegoUserPrincipal servicePrincipal = new SpnegoUserPrincipal(TEST_SERVICE_USER, ENCODED_TOKEN); + SPNEGOUserPrincipal servicePrincipal = new SPNEGOUserPrincipal(TEST_SERVICE_USER, ENCODED_TOKEN); UserIdentity serviceDelegate = mock(UserIdentity.class); Subject subject = new Subject(true, Collections.singleton(servicePrincipal), Collections.emptySet(), Collections.emptySet()); - SpnegoUserIdentity result = new SpnegoUserIdentity(subject, servicePrincipal, serviceDelegate); - expect(_mockSpnegoLoginService.login(anyString(), anyObject(), anyObject())).andReturn(result); + RoleDelegateUserIdentity result = new RoleDelegateUserIdentity(subject, servicePrincipal, serviceDelegate); + expect(_mockSpnegoLoginService.login(anyString(), anyObject(), anyObject(), anyObject())).andReturn(result); TestAuthorizer userAuthorizer = new TestAuthorizer(TEST_USER); - HttpServletRequest mockRequest = mock(HttpServletRequest.class); - expect(mockRequest.getParameter(DO_AS)).andReturn(TEST_USER); - - replay(_mockSpnegoLoginService, mockRequest); + Request mockRequest = mock(Request.class); + Context mockContext = mock(Context.class); + HttpURI uri = HttpURI.from("http://cruisecontrol.mycompany.com/somePath?" + DO_AS + "=" + TEST_USER); + expect(mockRequest.getHttpURI()).andReturn(uri); + expect(mockRequest.getContext()).andReturn(mockContext).anyTimes(); + expect(mockContext.getAttribute(anyString())).andReturn(null).anyTimes(); + ConnectionMetaData mockConnectionMetaData = mock(ConnectionMetaData.class); + HttpConfiguration mockConfig = mock(HttpConfiguration.class); + expect(mockRequest.getConnectionMetaData()).andReturn(mockConnectionMetaData).anyTimes(); + expect(mockConnectionMetaData.getHttpConfiguration()).andReturn(mockConfig).anyTimes(); + expect(mockConfig.getFormEncodedMethods()).andReturn(Set.of("")); + expect(mockRequest.getMethod()).andReturn("GET").anyTimes(); + expect(mockRequest.getAttribute(anyString())).andReturn(null).anyTimes(); + IdentityService mockIdentityService = mock(IdentityService.class); + expect(_mockSpnegoLoginService.getIdentityService()).andReturn(mockIdentityService); + expect(mockIdentityService.newUserIdentity(anyObject(), anyObject(), anyObject())).andReturn(serviceDelegate); + + replay(_mockSpnegoLoginService, mockRequest, mockContext, mockConnectionMetaData, mockConfig, mockIdentityService); TrustedProxyLoginService trustedProxyLoginService = new TrustedProxyLoginService(_mockSpnegoLoginService, _mockFallbackLoginService, userAuthorizer, false); - UserIdentity doAsIdentity = trustedProxyLoginService.login(null, ENCODED_TOKEN, mockRequest); + UserIdentity doAsIdentity = trustedProxyLoginService.login(null, ENCODED_TOKEN, mockRequest, null); assertNotNull(doAsIdentity); assertNotNull(doAsIdentity.getUserPrincipal()); - assertEquals(doAsIdentity.getUserPrincipal().getName(), TEST_USER); + assertEquals(TEST_USER, doAsIdentity.getUserPrincipal().getName()); assertEquals(((TrustedProxyPrincipal) doAsIdentity.getUserPrincipal()).servicePrincipal(), servicePrincipal); - verify(_mockSpnegoLoginService, mockRequest); + verify(_mockSpnegoLoginService, mockRequest, mockContext, mockConnectionMetaData, mockConfig); } @Test public void testNoDoAsUser() { - SpnegoUserPrincipal servicePrincipal = new SpnegoUserPrincipal(TEST_SERVICE_USER, ENCODED_TOKEN); + SPNEGOUserPrincipal servicePrincipal = new SPNEGOUserPrincipal(TEST_SERVICE_USER, ENCODED_TOKEN); UserIdentity serviceDelegate = mock(UserIdentity.class); Subject subject = new Subject(true, Collections.singleton(servicePrincipal), Collections.emptySet(), Collections.emptySet()); - SpnegoUserIdentity result = new SpnegoUserIdentity(subject, servicePrincipal, serviceDelegate); - expect(_mockSpnegoLoginService.login(anyString(), anyObject(), anyObject())).andReturn(result); + RoleDelegateUserIdentity result = new RoleDelegateUserIdentity(subject, servicePrincipal, serviceDelegate); + expect(_mockSpnegoLoginService.login(anyString(), anyObject(), anyObject(), anyObject())).andReturn(result); TestAuthorizer userAuthorizer = new TestAuthorizer(TEST_USER); - HttpServletRequest mockRequest = mock(HttpServletRequest.class); - replay(_mockSpnegoLoginService); + Request mockRequest = mock(Request.class); + Context mockContext = mock(Context.class); + HttpURI uri = HttpURI.from("http://cruisecontrol.mycompany.com/somePath"); + expect(mockRequest.getHttpURI()).andReturn(uri); + expect(mockRequest.getContext()).andReturn(mockContext).anyTimes(); + expect(mockContext.getAttribute(anyString())).andReturn(null).anyTimes(); + ConnectionMetaData mockConnectionMetaData = mock(ConnectionMetaData.class); + HttpConfiguration mockConfig = mock(HttpConfiguration.class); + expect(mockRequest.getConnectionMetaData()).andReturn(mockConnectionMetaData).anyTimes(); + expect(mockConnectionMetaData.getHttpConfiguration()).andReturn(mockConfig).anyTimes(); + expect(mockConfig.getFormEncodedMethods()).andReturn(Set.of("")); + expect(mockRequest.getMethod()).andReturn("GET").anyTimes(); + expect(mockRequest.getAttribute(anyString())).andReturn(null).anyTimes(); + replay(_mockSpnegoLoginService, mockRequest, mockContext, mockConnectionMetaData, mockConfig); TrustedProxyLoginService trustedProxyLoginService = new TrustedProxyLoginService(_mockSpnegoLoginService, _mockFallbackLoginService, userAuthorizer, false); - UserIdentity doAsIdentity = trustedProxyLoginService.login(null, ENCODED_TOKEN, mockRequest); + UserIdentity doAsIdentity = trustedProxyLoginService.login(null, ENCODED_TOKEN, mockRequest, null); assertNotNull(doAsIdentity); assertNotNull(doAsIdentity.getUserPrincipal()); assertNull(doAsIdentity.getUserPrincipal().getName()); - assertFalse(((SpnegoUserIdentity) doAsIdentity).isEstablished()); - verify(_mockSpnegoLoginService); + assertFalse(((RoleDelegateUserIdentity) doAsIdentity).isEstablished()); + verify(_mockSpnegoLoginService, mockRequest, mockContext, mockConnectionMetaData, mockConfig); } @Test public void testInvalidAuthServiceUser() { - SpnegoUserPrincipal servicePrincipal = new SpnegoUserPrincipal(TEST_SERVICE_USER, ENCODED_TOKEN); + SPNEGOUserPrincipal servicePrincipal = new SPNEGOUserPrincipal(TEST_SERVICE_USER, ENCODED_TOKEN); Subject subject = new Subject(true, Collections.singleton(servicePrincipal), Collections.emptySet(), Collections.emptySet()); - SpnegoUserIdentity result = new SpnegoUserIdentity(subject, servicePrincipal, null); - expect(_mockSpnegoLoginService.login(anyString(), anyObject(), anyObject())).andReturn(result); + RoleDelegateUserIdentity result = new RoleDelegateUserIdentity(subject, servicePrincipal, null); + expect(_mockSpnegoLoginService.login(anyString(), anyObject(), anyObject(), anyObject())).andReturn(result); TestAuthorizer userAuthorizer = new TestAuthorizer(TEST_USER); - HttpServletRequest mockRequest = mock(HttpServletRequest.class); - expect(mockRequest.getParameter(DO_AS)).andReturn(TEST_USER); - replay(_mockSpnegoLoginService); + Request mockRequest = mock(Request.class); + Context mockContext = mock(Context.class); + HttpURI uri = HttpURI.from("http://cruisecontrol.mycompany.com/somePath?" + DO_AS + "=" + TEST_USER); + expect(mockRequest.getHttpURI()).andReturn(uri); + expect(mockRequest.getContext()).andReturn(mockContext).anyTimes(); + expect(mockContext.getAttribute(anyString())).andReturn(null).anyTimes(); + ConnectionMetaData mockConnectionMetaData = mock(ConnectionMetaData.class); + HttpConfiguration mockConfig = mock(HttpConfiguration.class); + expect(mockRequest.getConnectionMetaData()).andReturn(mockConnectionMetaData).anyTimes(); + expect(mockConnectionMetaData.getHttpConfiguration()).andReturn(mockConfig).anyTimes(); + expect(mockConfig.getFormEncodedMethods()).andReturn(Set.of("")); + expect(mockRequest.getMethod()).andReturn("GET").anyTimes(); + expect(mockRequest.getAttribute(anyString())).andReturn(null).anyTimes(); + IdentityService mockIdentityService = mock(IdentityService.class); + expect(_mockSpnegoLoginService.getIdentityService()).andReturn(mockIdentityService); + expect(mockIdentityService.newUserIdentity(anyObject(), anyObject(), anyObject())).andReturn(null); + replay(_mockSpnegoLoginService, mockRequest, mockContext, mockConnectionMetaData, mockConfig, mockIdentityService); TrustedProxyLoginService trustedProxyLoginService = new TrustedProxyLoginService(_mockSpnegoLoginService, _mockFallbackLoginService, userAuthorizer, false); - UserIdentity doAsIdentity = trustedProxyLoginService.login(null, ENCODED_TOKEN, mockRequest); + UserIdentity doAsIdentity = trustedProxyLoginService.login(null, ENCODED_TOKEN, mockRequest, null); assertNotNull(doAsIdentity); - assertFalse(((SpnegoUserIdentity) doAsIdentity).isEstablished()); + assertFalse(((RoleDelegateUserIdentity) doAsIdentity).isEstablished()); } @Test public void testFallbackToSpnego() { - SpnegoUserPrincipal servicePrincipal = new SpnegoUserPrincipal(TEST_SERVICE_USER, ENCODED_TOKEN); + SPNEGOUserPrincipal servicePrincipal = new SPNEGOUserPrincipal(TEST_SERVICE_USER, ENCODED_TOKEN); UserIdentity serviceDelegate = mock(UserIdentity.class); Subject subject = new Subject(true, Collections.singleton(servicePrincipal), Collections.emptySet(), Collections.emptySet()); - SpnegoUserIdentity result = new SpnegoUserIdentity(subject, servicePrincipal, serviceDelegate); - expect(_mockFallbackLoginService.login(anyString(), anyObject(), anyObject())).andReturn(result); + RoleDelegateUserIdentity result = new RoleDelegateUserIdentity(subject, servicePrincipal, serviceDelegate); + expect(_mockFallbackLoginService.login(anyString(), anyObject(), anyObject(), anyObject())).andReturn(result); TestAuthorizer userAuthorizer = new TestAuthorizer(TEST_USER); - HttpServletRequest mockRequest = mock(HttpServletRequest.class); - replay(_mockFallbackLoginService); + Request mockRequest = mock(Request.class); + Context mockContext = mock(Context.class); + HttpURI uri = HttpURI.from("http://cruisecontrol.mycompany.com/somePath"); + expect(mockRequest.getHttpURI()).andReturn(uri); + expect(mockRequest.getContext()).andReturn(mockContext).anyTimes(); + expect(mockContext.getAttribute(anyString())).andReturn(null).anyTimes(); + ConnectionMetaData mockConnectionMetaData = mock(ConnectionMetaData.class); + HttpConfiguration mockConfig = mock(HttpConfiguration.class); + expect(mockRequest.getConnectionMetaData()).andReturn(mockConnectionMetaData).anyTimes(); + expect(mockConnectionMetaData.getHttpConfiguration()).andReturn(mockConfig).anyTimes(); + expect(mockConfig.getFormEncodedMethods()).andReturn(Set.of("")); + expect(mockRequest.getMethod()).andReturn("GET").anyTimes(); + expect(mockRequest.getAttribute(anyString())).andReturn(null).anyTimes(); + replay(_mockSpnegoLoginService, _mockFallbackLoginService, mockRequest, mockContext, mockConnectionMetaData, mockConfig); TrustedProxyLoginService trustedProxyLoginService = new TrustedProxyLoginService(_mockSpnegoLoginService, _mockFallbackLoginService, userAuthorizer, true); - UserIdentity doAsIdentity = trustedProxyLoginService.login(null, ENCODED_TOKEN, mockRequest); + UserIdentity doAsIdentity = trustedProxyLoginService.login(null, ENCODED_TOKEN, mockRequest, null); assertNotNull(doAsIdentity); assertNotNull(doAsIdentity.getUserPrincipal()); assertEquals(servicePrincipal, doAsIdentity.getUserPrincipal()); - verify(_mockFallbackLoginService); + verify(_mockSpnegoLoginService, _mockFallbackLoginService, mockRequest, mockContext, mockConnectionMetaData, mockConfig); } @Test public void testTrustedProxyWithKerberosRules() { String username = "user1"; String proxy = "proxy2@realm"; - SpnegoUserPrincipal servicePrincipal = new SpnegoUserPrincipal(proxy, ENCODED_TOKEN); + SPNEGOUserPrincipal servicePrincipal = new SPNEGOUserPrincipal(proxy, ENCODED_TOKEN); UserIdentity serviceDelegate = mock(UserIdentity.class); Subject subject = new Subject(true, Collections.singleton(servicePrincipal), Collections.emptySet(), Collections.emptySet()); - SpnegoUserIdentity result = new SpnegoUserIdentity(subject, servicePrincipal, serviceDelegate); - expect(_mockSpnegoLoginService.login(anyString(), anyObject(), anyObject())).andReturn(result); + RoleDelegateUserIdentity result = new RoleDelegateUserIdentity(subject, servicePrincipal, serviceDelegate); + expect(_mockSpnegoLoginService.login(anyString(), anyObject(), anyObject(), anyObject())).andReturn(result); TestAuthorizer userAuthorizer = new TestAuthorizer(username); - HttpServletRequest mockRequest = mock(HttpServletRequest.class); - expect(mockRequest.getParameter(DO_AS)).andReturn(username); - replay(_mockSpnegoLoginService, mockRequest); + Request mockRequest = mock(Request.class); + Context mockContext = mock(Context.class); + HttpURI uri = HttpURI.from("http://cruisecontrol.mycompany.com/somePath?" + DO_AS + "=" + username); + expect(mockRequest.getHttpURI()).andReturn(uri); + expect(mockRequest.getContext()).andReturn(mockContext).anyTimes(); + expect(mockContext.getAttribute(anyString())).andReturn(null).anyTimes(); + ConnectionMetaData mockConnectionMetaData = mock(ConnectionMetaData.class); + HttpConfiguration mockConfig = mock(HttpConfiguration.class); + expect(mockRequest.getConnectionMetaData()).andReturn(mockConnectionMetaData).anyTimes(); + expect(mockConnectionMetaData.getHttpConfiguration()).andReturn(mockConfig).anyTimes(); + expect(mockConfig.getFormEncodedMethods()).andReturn(Set.of("")); + expect(mockRequest.getMethod()).andReturn("GET").anyTimes(); + expect(mockRequest.getAttribute(anyString())).andReturn(null).anyTimes(); + IdentityService mockIdentityService = mock(IdentityService.class); + expect(_mockSpnegoLoginService.getIdentityService()).andReturn(mockIdentityService); + expect(mockIdentityService.newUserIdentity(anyObject(), anyObject(), anyObject())).andReturn(serviceDelegate); + replay(_mockSpnegoLoginService, mockRequest, mockContext, mockConnectionMetaData, mockConfig, mockIdentityService); TrustedProxyLoginService trustedProxyLoginService = new TrustedProxyLoginService(_mockSpnegoLoginService, _mockFallbackLoginService, userAuthorizer, false); - UserIdentity doAsIdentity = trustedProxyLoginService.login(proxy, ENCODED_TOKEN, mockRequest); + UserIdentity doAsIdentity = trustedProxyLoginService.login(proxy, ENCODED_TOKEN, mockRequest, null); assertNotNull(doAsIdentity); assertNotNull(doAsIdentity.getUserPrincipal()); - assertEquals(doAsIdentity.getUserPrincipal().getName(), username); + assertEquals(username, doAsIdentity.getUserPrincipal().getName()); assertEquals(((TrustedProxyPrincipal) doAsIdentity.getUserPrincipal()).servicePrincipal(), servicePrincipal); - verify(_mockSpnegoLoginService, mockRequest); + verify(_mockSpnegoLoginService, mockRequest, mockContext, mockConnectionMetaData, mockConfig, mockIdentityService); } @Test @@ -174,26 +252,38 @@ public void testFallbackToSpnegoWithKerberosRules() { String username = "user1"; String principal = "user1@realm"; String usernameReplaced = username + "foo"; - SpnegoUserPrincipal servicePrincipal = new SpnegoUserPrincipal(usernameReplaced, ENCODED_TOKEN); + SPNEGOUserPrincipal servicePrincipal = new SPNEGOUserPrincipal(usernameReplaced, ENCODED_TOKEN); UserIdentity serviceDelegate = mock(UserIdentity.class); Subject subject = new Subject(true, Collections.singleton(servicePrincipal), Collections.emptySet(), Collections.emptySet()); - SpnegoUserIdentity result = new SpnegoUserIdentity(subject, servicePrincipal, serviceDelegate); - expect(_mockFallbackLoginService.login(anyString(), anyObject(), anyObject())).andReturn(result); + RoleDelegateUserIdentity result = new RoleDelegateUserIdentity(subject, servicePrincipal, serviceDelegate); + expect(_mockFallbackLoginService.login(anyString(), anyObject(), anyObject(), anyObject())).andReturn(result); TestAuthorizer userAuthorizer = new TestAuthorizer(username); - HttpServletRequest mockRequest = mock(HttpServletRequest.class); - replay(_mockFallbackLoginService); + Request mockRequest = mock(Request.class); + Context mockContext = mock(Context.class); + HttpURI uri = HttpURI.from("http://cruisecontrol.mycompany.com/somePath"); + expect(mockRequest.getHttpURI()).andReturn(uri); + expect(mockRequest.getContext()).andReturn(mockContext).anyTimes(); + expect(mockContext.getAttribute(anyString())).andReturn(null).anyTimes(); + ConnectionMetaData mockConnectionMetaData = mock(ConnectionMetaData.class); + HttpConfiguration mockConfig = mock(HttpConfiguration.class); + expect(mockRequest.getConnectionMetaData()).andReturn(mockConnectionMetaData).anyTimes(); + expect(mockConnectionMetaData.getHttpConfiguration()).andReturn(mockConfig).anyTimes(); + expect(mockConfig.getFormEncodedMethods()).andReturn(Set.of("")); + expect(mockRequest.getMethod()).andReturn("GET").anyTimes(); + expect(mockRequest.getAttribute(anyString())).andReturn(null).anyTimes(); + replay(_mockSpnegoLoginService, _mockFallbackLoginService, mockRequest, mockContext, mockConnectionMetaData, mockConfig); TrustedProxyLoginService trustedProxyLoginService = new TrustedProxyLoginService(_mockSpnegoLoginService, _mockFallbackLoginService, userAuthorizer, true); - UserIdentity doAsIdentity = trustedProxyLoginService.login(principal, ENCODED_TOKEN, mockRequest); + UserIdentity doAsIdentity = trustedProxyLoginService.login(principal, ENCODED_TOKEN, mockRequest, null); assertNotNull(doAsIdentity); assertNotNull(doAsIdentity.getUserPrincipal()); - SpnegoUserPrincipal doAsPrincipal = (SpnegoUserPrincipal) doAsIdentity.getUserPrincipal(); + SPNEGOUserPrincipal doAsPrincipal = (SPNEGOUserPrincipal) doAsIdentity.getUserPrincipal(); assertEquals(servicePrincipal.getName(), doAsPrincipal.getName()); - assertTrue(((SpnegoUserIdentity) doAsIdentity).isEstablished()); - verify(_mockFallbackLoginService); + assertTrue(((RoleDelegateUserIdentity) doAsIdentity).isEstablished()); + verify(_mockSpnegoLoginService, _mockFallbackLoginService, mockRequest, mockContext, mockConnectionMetaData, mockConfig); } } diff --git a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxySecurityProviderTestUtils.java b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxySecurityProviderTestUtils.java index f8e43975c6..fec165d1f6 100644 --- a/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxySecurityProviderTestUtils.java +++ b/cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/servlet/security/trustedproxy/TrustedProxySecurityProviderTestUtils.java @@ -5,8 +5,8 @@ import com.linkedin.kafka.cruisecontrol.KafkaCruiseControlApp; import com.linkedin.kafka.cruisecontrol.servlet.security.MiniKdc; +import jakarta.servlet.http.HttpServletResponse; import javax.security.auth.Subject; -import javax.servlet.http.HttpServletResponse; import java.net.HttpURLConnection; import java.net.URI; import java.security.PrivilegedAction; @@ -14,7 +14,6 @@ import static com.linkedin.kafka.cruisecontrol.servlet.CruiseControlEndPoint.STATE; import static com.linkedin.kafka.cruisecontrol.servlet.parameters.ParameterUtils.DO_AS; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; /** * A test util class. @@ -50,13 +49,10 @@ public static void testNoDoAsParameter(MiniKdc miniKdc, KafkaCruiseControlApp ap try { stateEndpointConnection = (HttpURLConnection) new URI(app.serverUrl()) .resolve(CRUISE_CONTROL_STATE_ENDPOINT).toURL().openConnection(); + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, stateEndpointConnection.getResponseCode()); } catch (Exception e) { throw new RuntimeException(e); } - // There is a bug in the Jetty implementation, and it doesn't seem to handle the connection - // properly in case of an error, so it somehow doesn't send a response code. To work this around - // I catch the RuntimeException that it throws. - assertThrows(RuntimeException.class, stateEndpointConnection::getResponseCode); return null; }); } @@ -70,13 +66,10 @@ public static void testNotAdminServiceLogin(MiniKdc miniKdc, KafkaCruiseControlA try { stateEndpointConnection = (HttpURLConnection) new URI(app.serverUrl()) .resolve(endpoint).toURL().openConnection(); + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, stateEndpointConnection.getResponseCode()); } catch (Exception e) { throw new RuntimeException(e); } - // There is a bug in the Jetty implementation, and it doesn't seem to handle the connection - // properly in case of an error, so it somehow doesn't send a response code. To work this around - // I catch the RuntimeException that it throws. - assertThrows(RuntimeException.class, stateEndpointConnection::getResponseCode); return null; }); } @@ -105,13 +98,10 @@ public static void testUnsuccessfulFallbackAuthentication(MiniKdc miniKdc, Kafka try { stateEndpointConnection = (HttpURLConnection) new URI(app.serverUrl()) .resolve(CRUISE_CONTROL_STATE_ENDPOINT).toURL().openConnection(); + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, stateEndpointConnection.getResponseCode()); } catch (Exception e) { throw new RuntimeException(e); } - // There is a bug in the Jetty implementation, and it doesn't seem to handle the connection - // properly in case of an error, so it somehow doesn't send a response code. To work this around - // I catch the RuntimeException that it throws. - assertThrows(RuntimeException.class, stateEndpointConnection::getResponseCode); return null; }); } diff --git a/gradle.properties b/gradle.properties index a36658f42c..2b0903a596 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,5 +4,5 @@ org.gradle.jvmargs=-Xms512m -Xmx512m scalaVersion=2.13.13 kafkaVersion=3.9.1 nettyVersion=4.1.118.Final -jettyVersion=9.4.56.v20240826 +jettyVersion=12.0.12 vertxVersion=4.5.8