Skip to content

Commit c73b932

Browse files
committed
Implement review comments
Signed-off-by: JvD_Ericsson <jeff.van.dam@est.tech>
1 parent bf477d8 commit c73b932

25 files changed

Lines changed: 347 additions & 634 deletions

build.gradle

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ project(':cruise-control') {
303303
implementation 'com.google.code.gson:gson:2.9.0'
304304
implementation "org.eclipse.jetty:jetty-server:${jettyVersion}"
305305
implementation 'io.dropwizard.metrics:metrics-jmx:4.2.9'
306-
implementation 'com.nimbusds:nimbus-jose-jwt:9.45'
306+
implementation 'com.nimbusds:nimbus-jose-jwt:10.0.2'
307307
implementation 'io.swagger.parser.v3:swagger-parser-v3:2.1.16'
308308
implementation 'io.github.classgraph:classgraph:4.8.141'
309309
implementation 'com.google.code.findbugs:jsr305:3.0.2'
@@ -339,6 +339,7 @@ project(':cruise-control') {
339339
testImplementation 'com.jayway.jsonpath:json-path:2.7.0'
340340
testImplementation 'org.powermock:powermock-module-junit4:2.0.9'
341341
testImplementation 'org.powermock:powermock-api-easymock:2.0.9'
342+
testImplementation "org.testcontainers:kafka:$testcontainersVersion"
342343
}
343344

344345
publishing {
@@ -483,7 +484,7 @@ project(':cruise-control-metrics-reporter') {
483484
testImplementation "org.apache.kafka:kafka-raft:$kafkaVersion"
484485
testImplementation "org.apache.kafka:kafka-storage:$kafkaVersion"
485486
testImplementation 'commons-io:commons-io:2.11.0'
486-
testImplementation "org.testcontainers:kafka:1.21.3"
487+
testImplementation "org.testcontainers:kafka:$testcontainersVersion"
487488
testOutput sourceSets.test.output
488489
}
489490

cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/KafkaCruiseControlServletApp.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import org.eclipse.jetty.ee10.servlet.ServletHolder;
2323
import org.eclipse.jetty.util.ssl.SslContextFactory;
2424
import jakarta.servlet.ServletException;
25+
import java.nio.file.Files;
26+
import java.nio.file.Path;
2527
import java.util.List;
2628

2729
public class KafkaCruiseControlServletApp extends KafkaCruiseControlApp {
@@ -120,12 +122,17 @@ private void maybeConfigureTlsProtocolsAndCiphers(SslContextFactory sslContextFa
120122
protected void setupWebUi(ServletContextHandler contextHandler) {
121123
// Placeholder for any static content
122124
String webuiDir = _config.getString(WebServerConfig.WEBSERVER_UI_DISKPATH_CONFIG);
123-
String webuiPathPrefix = _config.getString(WebServerConfig.WEBSERVER_UI_URLPREFIX_CONFIG);
124-
DefaultServlet defaultServlet = new DefaultServlet();
125-
ServletHolder holderWebapp = new ServletHolder("default", defaultServlet);
126-
// holderWebapp.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false");
127-
holderWebapp.setInitParameter("baseResource", webuiDir);
128-
contextHandler.addServlet(holderWebapp, webuiPathPrefix);
125+
Path path = Path.of(webuiDir);
126+
if (Files.isDirectory(path) && Files.isReadable(path)) {
127+
String webUiPathPrefix = _config.getString(WebServerConfig.WEBSERVER_UI_URLPREFIX_CONFIG);
128+
DefaultServlet defaultServlet = new DefaultServlet();
129+
ServletHolder holderWebapp = new ServletHolder("default", defaultServlet);
130+
// holderWebapp.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false");
131+
contextHandler.setBaseResourceAsString(webuiDir);
132+
contextHandler.addServlet(holderWebapp, webUiPathPrefix);
133+
} else {
134+
LOG.warn("WebUI directory not found or unreadable: {} UI disabled", webuiDir);
135+
}
129136
}
130137

131138
protected ServletContextHandler createContextHandler() {

cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/UserPermissionsManager.java

Lines changed: 19 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,16 @@
44

55
package com.linkedin.kafka.cruisecontrol.servlet;
66

7-
import java.io.BufferedReader;
8-
import java.io.IOException;
9-
import java.io.InputStreamReader;
10-
import java.io.UncheckedIOException;
11-
import java.nio.charset.StandardCharsets;
127
import java.util.Map;
138
import java.util.Set;
149
import java.util.HashMap;
15-
import java.util.HashSet;
1610
import java.util.stream.Collectors;
1711
import java.util.Collections;
1812
import com.linkedin.kafka.cruisecontrol.config.KafkaCruiseControlConfig;
13+
import org.eclipse.jetty.security.PropertyUserStore;
1914
import org.eclipse.jetty.security.RolePrincipal;
2015
import org.eclipse.jetty.security.UserStore;
21-
import org.eclipse.jetty.security.PropertyUserStore;
2216
import com.linkedin.kafka.cruisecontrol.config.constants.WebServerConfig;
23-
import org.eclipse.jetty.server.handler.ResourceHandler;
2417
import org.eclipse.jetty.util.resource.Resource;
2518
import org.eclipse.jetty.util.resource.ResourceFactory;
2619
import org.slf4j.Logger;
@@ -50,16 +43,14 @@ private Map<String, Set<String>> createRolesPerUsersMap() {
5043
boolean securityEnabled = _config.getBoolean(WebServerConfig.WEBSERVER_SECURITY_ENABLE_CONFIG);
5144
if (securityEnabled) {
5245
String privilegesFilePath = _config.getString(WebServerConfig.WEBSERVER_AUTH_CREDENTIALS_FILE_CONFIG);
53-
Resource resource = ResourceFactory.of(new ResourceHandler()).newResource(privilegesFilePath);
54-
UserStore userStore = createUserStoreFromResource(resource);
46+
Resource resource = ResourceFactory.root().newResource(privilegesFilePath);
47+
ExposedPropertyUserStore userStore = createUserStoreFromResource(resource);
5548
startUserStore(userStore);
5649

57-
Set<String> userNames = parseUsernames(resource);
50+
Set<String> userNames = userStore.getUsersNames();
5851

5952
for (String user : userNames) {
60-
Set<RolePrincipal> roles = new HashSet<>(userStore.getRolePrincipals(user));
61-
62-
Set<String> roleNames = roles.stream()
53+
Set<String> roleNames = userStore.getRolePrincipals(user).stream()
6354
.map(RolePrincipal::getName)
6455
.map(String::toUpperCase)
6556
.collect(Collectors.toSet());
@@ -99,43 +90,25 @@ public Set<String> getRolesBy(String userName) {
9990

10091
/** Creates UserStore from an external file
10192
*
102-
* @param privilegedResource a filepath containing user privileges information
93+
* @param privilegesResource a filepath containing user privileges information
10394
* @return a UserStore object
10495
*/
105-
private UserStore createUserStoreFromResource(Resource privilegedResource) {
106-
PropertyUserStore userStore = new PropertyUserStore();
107-
userStore.setConfig(privilegedResource);
96+
private ExposedPropertyUserStore createUserStoreFromResource(Resource privilegesResource) {
97+
ExposedPropertyUserStore userStore = new ExposedPropertyUserStore();
98+
userStore.setConfig(privilegesResource);
10899
return userStore;
109100
}
110101

111-
/** Creates a set of usernames from a Resource
112-
*
113-
* @param resource a Resource containing user privileges information
114-
* @return a Set of usernames parsed from the Resource
115-
*/
116-
private static Set<String> parseUsernames(Resource resource) {
117-
if (!resource.exists() || !resource.isReadable()) {
118-
return Set.of();
119-
}
120-
Set<String> usernames = new HashSet<>();
121-
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.newInputStream(), StandardCharsets.UTF_8))) {
122-
String line;
123-
while ((line = reader.readLine()) != null) {
124-
line = line.trim();
125-
if (line.isEmpty() || line.startsWith("#")) {
126-
continue;
127-
}
128-
int colonIndex = line.indexOf(':');
129-
if (colonIndex != -1) {
130-
String username = line.substring(0, colonIndex).trim();
131-
if (!username.isEmpty()) {
132-
usernames.add(username);
133-
}
134-
}
135-
}
136-
} catch (IOException e) {
137-
throw new UncheckedIOException("Failed to read usernames from " + resource, e);
102+
private static class ExposedPropertyUserStore extends PropertyUserStore {
103+
104+
/**
105+
* This method exposes the protected `_users` map inherited from
106+
* UserStore, providing direct access to the stored users' names.
107+
*
108+
* @return the set of users' names
109+
*/
110+
public Set<String> getUsersNames() {
111+
return _users.keySet();
138112
}
139-
return usernames;
140113
}
141114
}

cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/BasicSecurityProvider.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import org.eclipse.jetty.security.HashLoginService;
1111
import org.eclipse.jetty.security.LoginService;
1212
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
13-
import org.eclipse.jetty.server.handler.ResourceHandler;
1413
import org.eclipse.jetty.util.resource.Resource;
1514
import org.eclipse.jetty.util.resource.ResourceFactory;
1615

@@ -30,7 +29,7 @@ public void init(KafkaCruiseControlConfig config) {
3029

3130
@Override
3231
public LoginService loginService() {
33-
Resource resource = ResourceFactory.of(new ResourceHandler()).newResource(_userCredentialsFile);
32+
Resource resource = ResourceFactory.root().newResource(_userCredentialsFile);
3433
return new HashLoginService("DefaultLoginService", resource);
3534
}
3635

cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/DefaultRoleSecurityProvider.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.util.List;
1212
import java.util.Set;
1313
import org.eclipse.jetty.ee10.servlet.security.ConstraintMapping;
14+
import org.eclipse.jetty.security.Authenticator;
1415
import org.eclipse.jetty.security.Constraint;
1516

1617

@@ -65,8 +66,11 @@ public Set<String> roles() {
6566
}
6667

6768
private ConstraintMapping mapping(CruiseControlEndPoint endpoint, String... roles) {
68-
Constraint.Builder builder = new Constraint.Builder();
69-
Constraint constraint = builder.roles(roles).name("BASIC").authorization(Constraint.Authorization.SPECIFIC_ROLE).build();
69+
Constraint constraint = new Constraint.Builder()
70+
.name(Authenticator.BASIC_AUTH)
71+
.roles(roles)
72+
.authorization(Constraint.Authorization.SPECIFIC_ROLE)
73+
.build();
7074
ConstraintMapping mapping = new ConstraintMapping();
7175
mapping.setPathSpec(_webServerApiUrlPrefix.replace("*", endpoint.name().toLowerCase()));
7276
mapping.setConstraint(constraint);

cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/DummyLoginService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ public void logout(UserIdentity user) {
4545
// dummy implementation
4646
}
4747

48-
@Override
49-
public UserIdentity login(String u, Object c, Request req, Function<Boolean, Session> getOrCreateSession) {
48+
@Override
49+
public UserIdentity login(String u, Object c, Request req, Function<Boolean, Session> getOrCreateSession) {
5050
return null;
5151
}
5252

cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/RoleProvider.java

Lines changed: 0 additions & 21 deletions
This file was deleted.

cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/UserStoreRoleProvider.java

Lines changed: 0 additions & 61 deletions
This file was deleted.

cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/servlet/security/jwt/JwtAuthenticator.java

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@
1111
import org.eclipse.jetty.http.HttpStatus;
1212
import org.eclipse.jetty.http.HttpURI;
1313
import org.eclipse.jetty.security.AuthenticationState;
14-
import org.eclipse.jetty.security.ServerAuthException;
1514
import org.eclipse.jetty.security.UserIdentity;
1615
import org.eclipse.jetty.security.authentication.LoginAuthenticator;
16+
import org.eclipse.jetty.security.internal.DeferredAuthenticationState;
1717
import org.eclipse.jetty.server.Request;
1818
import org.eclipse.jetty.server.Response;
1919
import org.eclipse.jetty.util.Callback;
20-
import org.eclipse.jetty.util.URIUtil;
2120
import org.slf4j.Logger;
2221
import org.slf4j.LoggerFactory;
2322
import java.text.ParseException;
@@ -74,7 +73,10 @@ public class JwtAuthenticator extends LoginAuthenticator {
7473
public JwtAuthenticator(String authenticationProviderUrl, String cookieName) {
7574
_cookieName = cookieName;
7675
Function<String, Function<Request, String>> urlGen =
77-
url -> req -> url.replace(REDIRECT_URL, getRequestURL(req) + getOriginalQueryString(req));
76+
url -> req -> {
77+
HttpURI httpUri = HttpURI.build(req.getHttpURI()).query(null);
78+
return url.replace(REDIRECT_URL, httpUri.asString() + getOriginalQueryString(req));
79+
};
7880
_authenticationProviderUrlGenerator = urlGen.apply(authenticationProviderUrl);
7981
}
8082

@@ -84,13 +86,13 @@ public String getAuthenticationType() {
8486
}
8587

8688
@Override
87-
public AuthenticationState validateRequest(Request request, Response response, Callback callback) throws ServerAuthException {
89+
public AuthenticationState validateRequest(Request request, Response response, Callback callback) {
8890
JWT_LOGGER.trace("Authentication request received for " + request.toString());
8991

9092
String serializedJWT;
9193
// we'll skip the authentication for CORS preflight requests
9294
if (HttpMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod())) {
93-
return null;
95+
return new DeferredAuthenticationState(this);
9496
}
9597
serializedJWT = getJwtFromBearerAuthorization(request);
9698
if (serializedJWT == null) {
@@ -151,21 +153,4 @@ private String getOriginalQueryString(Request req) {
151153
String originalQueryString = req.getHttpURI().getQuery();
152154
return (originalQueryString == null) ? "" : "?" + originalQueryString;
153155
}
154-
155-
/**
156-
* Get the full request URL including scheme, host, port and path but excluding the query string.
157-
*
158-
* @param req is the request to process
159-
* @return the full request URL
160-
*/
161-
public String getRequestURL(Request req) {
162-
final StringBuilder url = new StringBuilder();
163-
HttpURI uri = req.getHttpURI();
164-
URIUtil.appendSchemeHostPort(url, uri.getScheme(), Request.getServerName(req), Request.getServerPort(req));
165-
String path = uri.getPath();
166-
if (path != null) {
167-
url.append(path);
168-
}
169-
return url.toString();
170-
}
171156
}

0 commit comments

Comments
 (0)