Skip to content

Commit b173f15

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

30 files changed

Lines changed: 437 additions & 440 deletions

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

Lines changed: 10 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,14 @@ 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+
contextHandler.setBaseResourceAsString(webuiDir);
131+
contextHandler.addServlet(holderWebapp, webuiPathPrefix);
132+
}
129133
}
130134

131135
protected ServletContextHandler createContextHandler() {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2025 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information.
3+
*/
4+
5+
package com.linkedin.kafka.cruisecontrol.servlet;
6+
7+
import org.eclipse.jetty.security.DefaultIdentityService;
8+
import org.eclipse.jetty.security.IdentityService;
9+
import org.eclipse.jetty.security.PropertyUserStore;
10+
import org.eclipse.jetty.security.RolePrincipal;
11+
import org.eclipse.jetty.security.UserIdentity;
12+
import org.eclipse.jetty.security.UserPrincipal;
13+
import java.util.List;
14+
import java.util.Set;
15+
16+
public class ExposedPropertyUserStore extends PropertyUserStore {
17+
18+
private IdentityService _identityService = new DefaultIdentityService();
19+
20+
/**
21+
* This method exposes the protected `_users` map inherited from
22+
* UserStore, providing direct access to the stored users' names.
23+
* @return the set of users' names
24+
*/
25+
public Set<String> getUsersNames() {
26+
return _users.keySet();
27+
}
28+
29+
/**
30+
* @param userName the user's name
31+
* This method gets the UserIdentity for a given username.
32+
* @return the UserIdentity
33+
*/
34+
public UserIdentity getUserIdentity(String userName) {
35+
UserPrincipal user = super.getUserPrincipal(userName);
36+
List<RolePrincipal> roles = super.getRolePrincipals(userName);
37+
return UserIdentityUtil.build(_identityService, user, roles);
38+
}
39+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2025 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information.
3+
*/
4+
5+
package com.linkedin.kafka.cruisecontrol.servlet;
6+
7+
import org.eclipse.jetty.security.DefaultIdentityService;
8+
import org.eclipse.jetty.security.IdentityService;
9+
import org.eclipse.jetty.security.RolePrincipal;
10+
import org.eclipse.jetty.security.UserIdentity;
11+
import org.eclipse.jetty.security.UserPrincipal;
12+
import org.eclipse.jetty.security.UserStore;
13+
import java.util.List;
14+
15+
public class ExposedUserStore extends UserStore {
16+
17+
private IdentityService _identityService = new DefaultIdentityService();
18+
19+
/**
20+
* @param userName the user's name
21+
* This method gets the UserIdentity for a given username.
22+
* @return the UserIdentity
23+
*/
24+
public UserIdentity getUserIdentity(String userName) {
25+
UserPrincipal user = super.getUserPrincipal(userName);
26+
List<RolePrincipal> roles = super.getRolePrincipals(userName);
27+
return UserIdentityUtil.build(_identityService, user, roles);
28+
}
29+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2025 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information.
3+
*/
4+
5+
package com.linkedin.kafka.cruisecontrol.servlet;
6+
7+
import org.eclipse.jetty.security.IdentityService;
8+
import org.eclipse.jetty.security.RolePrincipal;
9+
import org.eclipse.jetty.security.UserIdentity;
10+
import org.eclipse.jetty.security.UserPrincipal;
11+
import javax.security.auth.Subject;
12+
import java.security.Principal;
13+
import java.util.List;
14+
15+
final class UserIdentityUtil {
16+
17+
private UserIdentityUtil() { }
18+
19+
static UserIdentity build(IdentityService ids, UserPrincipal user, List<RolePrincipal> rolePrincipals) {
20+
if (user == null) {
21+
return null;
22+
}
23+
Subject subject = new Subject();
24+
subject.getPrincipals().add(user);
25+
subject.getPrincipals().addAll(rolePrincipals);
26+
27+
String[] roles = rolePrincipals.stream()
28+
.map(Principal::getName)
29+
.toArray(String[]::new);
30+
31+
return ids.newUserIdentity(subject, user, roles);
32+
}
33+
}

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

Lines changed: 8 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,15 @@
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;
1913
import org.eclipse.jetty.security.RolePrincipal;
2014
import org.eclipse.jetty.security.UserStore;
21-
import org.eclipse.jetty.security.PropertyUserStore;
2215
import com.linkedin.kafka.cruisecontrol.config.constants.WebServerConfig;
23-
import org.eclipse.jetty.server.handler.ResourceHandler;
2416
import org.eclipse.jetty.util.resource.Resource;
2517
import org.eclipse.jetty.util.resource.ResourceFactory;
2618
import org.slf4j.Logger;
@@ -50,16 +42,14 @@ private Map<String, Set<String>> createRolesPerUsersMap() {
5042
boolean securityEnabled = _config.getBoolean(WebServerConfig.WEBSERVER_SECURITY_ENABLE_CONFIG);
5143
if (securityEnabled) {
5244
String privilegesFilePath = _config.getString(WebServerConfig.WEBSERVER_AUTH_CREDENTIALS_FILE_CONFIG);
53-
Resource resource = ResourceFactory.of(new ResourceHandler()).newResource(privilegesFilePath);
54-
UserStore userStore = createUserStoreFromResource(resource);
45+
Resource resource = ResourceFactory.root().newResource(privilegesFilePath);
46+
ExposedPropertyUserStore userStore = createUserStoreFromResource(resource);
5547
startUserStore(userStore);
5648

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

5951
for (String user : userNames) {
60-
Set<RolePrincipal> roles = new HashSet<>(userStore.getRolePrincipals(user));
61-
62-
Set<String> roleNames = roles.stream()
52+
Set<String> roleNames = userStore.getRolePrincipals(user).stream()
6353
.map(RolePrincipal::getName)
6454
.map(String::toUpperCase)
6555
.collect(Collectors.toSet());
@@ -99,43 +89,12 @@ public Set<String> getRolesBy(String userName) {
9989

10090
/** Creates UserStore from an external file
10191
*
102-
* @param privilegedResource a filepath containing user privileges information
92+
* @param privilegesResource a filepath containing user privileges information
10393
* @return a UserStore object
10494
*/
105-
private UserStore createUserStoreFromResource(Resource privilegedResource) {
106-
PropertyUserStore userStore = new PropertyUserStore();
107-
userStore.setConfig(privilegedResource);
95+
private ExposedPropertyUserStore createUserStoreFromResource(Resource privilegesResource) {
96+
ExposedPropertyUserStore userStore = new ExposedPropertyUserStore();
97+
userStore.setConfig(privilegesResource);
10898
return userStore;
10999
}
110-
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);
138-
}
139-
return usernames;
140-
}
141100
}

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@
44

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

7+
import org.eclipse.jetty.security.UserIdentity;
78
import org.eclipse.jetty.server.Request;
89

910
/**
1011
* An interface to get roles for a given user.
1112
*/
12-
public interface RoleProvider {
13+
@FunctionalInterface
14+
public interface AuthorizationService {
1315
/**
14-
* Get the roles for a given user.
15-
*
16-
* @param request the request
17-
* @param username the username
18-
* @return the roles for the user or null if no roles found
16+
* @param request the current request
17+
* @param name the user name
18+
* @return a {@link UserIdentity} to query for roles of the given user
1919
*/
20-
String[] rolesFor(Request request, String username);
20+
UserIdentity getUserIdentity(Request request, String name);
2121
}

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2020 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information.
3+
*/
4+
5+
package com.linkedin.kafka.cruisecontrol.servlet.security;
6+
7+
import com.linkedin.kafka.cruisecontrol.servlet.ExposedPropertyUserStore;
8+
import org.eclipse.jetty.security.UserIdentity;
9+
import org.eclipse.jetty.server.Request;
10+
import org.eclipse.jetty.util.component.AbstractLifeCycle;
11+
import org.eclipse.jetty.util.resource.PathResourceFactory;
12+
import org.eclipse.jetty.util.resource.Resource;
13+
import java.nio.file.Path;
14+
15+
/**
16+
* Can be used for authorization scenarios where a file can be created in a secure location with a relatively
17+
* low number of users. It follows the <code>username: password [,rolename ...]</code> format which corresponds to
18+
* the format used with {@link org.eclipse.jetty.security.HashLoginService}.
19+
*/
20+
public class UserStoreAuthorizationService extends AbstractLifeCycle implements AuthorizationService {
21+
22+
private final ExposedPropertyUserStore _userStore;
23+
24+
public UserStoreAuthorizationService(String privilegesFilePath) {
25+
this(userStoreFromFile(privilegesFilePath));
26+
}
27+
28+
public UserStoreAuthorizationService(ExposedPropertyUserStore userStore) {
29+
_userStore = userStore;
30+
}
31+
32+
@Override
33+
public UserIdentity getUserIdentity(Request request, String name) {
34+
return _userStore.getUserIdentity(name);
35+
}
36+
37+
@Override
38+
protected void doStart() throws Exception {
39+
super.doStart();
40+
_userStore.start();
41+
}
42+
43+
@Override
44+
protected void doStop() throws Exception {
45+
_userStore.stop();
46+
super.doStop();
47+
}
48+
49+
private static ExposedPropertyUserStore userStoreFromFile(String privilegesFilePath) {
50+
ExposedPropertyUserStore userStore = new ExposedPropertyUserStore();
51+
Resource res = new PathResourceFactory().newResource(Path.of(privilegesFilePath));
52+
userStore.setConfig(res);
53+
return userStore;
54+
}
55+
}

0 commit comments

Comments
 (0)