Skip to content

Commit 9f945f8

Browse files
authored
KNOX-3358: Support configurable bind credentials for the embedded Knox LDAP service (#1275)
1 parent 24dccd7 commit 9f945f8

11 files changed

Lines changed: 244 additions & 10 deletions

File tree

gateway-docker/src/main/resources/docker/gateway-entrypoint.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,15 @@ else
110110
fi
111111
/home/knox/knox/bin/knoxcli.sh create-master --master "${MASTER_SECRET}"
112112

113-
if [[ -n ${LDAP_PASSWORD_FILE} ]]
114-
then
115-
LDAP_BIND_PASSWORD=$(/bin/cat "${LDAP_PASSWORD_FILE}" 2> /dev/null)
113+
# Check LDAP_BIND_PASSWORD first, and only fall back to the file if it’s unset or empty.
114+
if [[ -z ${LDAP_BIND_PASSWORD} && -n ${LDAP_PASSWORD_FILE} ]]; then
115+
LDAP_BIND_PASSWORD=$(/bin/cat "${LDAP_PASSWORD_FILE}" 2>/dev/null)
116116
fi
117117

118-
saveAlias ldap-bind-password "${LDAP_BIND_PASSWORD}"
119118
saveAlias gateway_database_user "${DATABASE_CONNECTION_USER}"
120119
saveAlias gateway_database_password "${DATABASE_CONNECTION_PASSWORD}"
121120
saveAlias gateway_database_ssl_truststore_password "${DATABASE_CONNECTION_TRUSTSTORE_PASSWORD}"
121+
saveAlias gateway_ldap_bind_password "${LDAP_BIND_PASSWORD}"
122122

123123
# RemoteAuthProvider truststore password
124124
saveAlias rap_truststore_password "${RAP_TRUSTSTORE_PASSWORD}"

gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1753,6 +1753,11 @@ public String getLDAPBaseDN() {
17531753
return get(LDAP_BASE_DN, "dc=proxy,dc=com");
17541754
}
17551755

1756+
@Override
1757+
public String getLDAPBindUser() {
1758+
return get(LDAP_BIND_USER, null);
1759+
}
1760+
17561761
@Override
17571762
public List<String> getLDAPInterceptorNames() {
17581763
return splitConfigValueToList(LDAP_INTERCEPTOR_NAMES);

gateway-server/src/main/java/org/apache/knox/gateway/services/factory/LdapServiceFactory.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ protected Service createService(GatewayServices gatewayServices, ServiceType ser
3737
KnoxLDAPService service = null;
3838
if (shouldCreateService(implementation)) {
3939
service = new KnoxLDAPService();
40+
service.setAliasService(getAliasService(gatewayServices));
4041
GatewayServer.registerConfigChangeListener(service);
4142
logServiceUsage(service.getClass().getName(), serviceType);
4243
}

gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
4747
import org.apache.knox.gateway.services.ldap.control.RolesLookupBypassControlFactory;
4848
import org.apache.knox.gateway.services.ldap.interceptor.InterceptorFactory;
49+
import org.apache.knox.gateway.services.security.AliasService;
4950

5051
import java.io.File;
5152
import java.util.ArrayList;
@@ -63,6 +64,8 @@
6364
*/
6465
public class KnoxLDAPServerManager {
6566
private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class);
67+
private static final String LDAP_BIND_PASSWORD_ALIAS = "gateway_ldap_bind_password";
68+
private final AliasService aliasService;
6669

6770
@VisibleForTesting
6871
DirectoryService directoryService;
@@ -72,9 +75,14 @@ public class KnoxLDAPServerManager {
7275
private File workDir;
7376
private int port;
7477
private String baseDn;
78+
private String bindUser;
7579
// Collection of DNs for the proxied backend LDAP servers
7680
private Set<String> baseDns;
7781

82+
KnoxLDAPServerManager(AliasService aliasService) {
83+
this.aliasService = aliasService;
84+
}
85+
7886
/**
7987
* Initialize the LDAP server with the given configuration
8088
*
@@ -90,6 +98,7 @@ public void initialize(GatewayConfig config) throws Exception {
9098
// Get configuration
9199
this.port = config.getLDAPPort();
92100
this.baseDn = config.getLDAPBaseDN();
101+
this.bindUser = config.getLDAPBindUser();
93102

94103
createInterceptors(config);
95104

@@ -177,15 +186,25 @@ public void start() throws Exception {
177186

178187
addInterceptors();
179188

180-
// Allow anonymous access
181-
directoryService.setAllowAnonymousAccess(true);
189+
// Require clients to bind with the configured credentials when both a bind user and
190+
// a bind password (resolved from the gateway credential store) are set; otherwise
191+
// keep the historical behavior of allowing anonymous access.
192+
final char[] bindPasswordChars = aliasService.getPasswordFromAliasForGateway(LDAP_BIND_PASSWORD_ALIAS);
193+
final String bindPassword = bindPasswordChars == null ? null : new String(bindPasswordChars);
194+
final boolean requireBind = StringUtils.isNotBlank(bindUser) && StringUtils.isNotBlank(bindPassword);
195+
directoryService.setAllowAnonymousAccess(!requireBind);
182196

183197
// Start the service
184198
directoryService.startup();
185199

186200
// Add base entries to the partitions
187201
createBaseEntries(baseDns, schemaManager);
188202

203+
if (requireBind) {
204+
createBindUser(schemaManager, bindPassword);
205+
LOG.ldapBindUserConfigured(bindUser);
206+
}
207+
189208
// Create LDAP server on configured port
190209
ldapServer = new LdapServer();
191210
ldapServer.setTransports(new TcpTransport(port));
@@ -318,6 +337,26 @@ private void createBaseEntriesForDn(SchemaManager schemaManager, String dn) thro
318337
}
319338
}
320339

340+
/**
341+
* Create the entry used by external clients to bind against the embedded LDAP server.
342+
* The bind DN's parent container (e.g. {@code ou=system} or {@code ou=people,<baseDn>})
343+
* must already exist. The entry is added using the privileged admin session, which is
344+
* unaffected by the anonymous-access setting.
345+
*/
346+
private void createBindUser(SchemaManager schemaManager, String bindPassword) throws Exception {
347+
Dn bindDn = new Dn(schemaManager, bindUser);
348+
if (!directoryService.getAdminSession().exists(bindDn)) {
349+
String rdnValue = bindDn.getRdn().getValue();
350+
Entry bindEntry = new DefaultEntry(schemaManager);
351+
bindEntry.setDn(bindDn);
352+
bindEntry.add("objectClass", "top", "person", "organizationalPerson", "inetOrgPerson");
353+
bindEntry.add("cn", rdnValue);
354+
bindEntry.add("sn", rdnValue);
355+
bindEntry.add("userPassword", bindPassword);
356+
directoryService.getAdminSession().add(bindEntry);
357+
}
358+
}
359+
321360
public int getPort() {
322361
return port;
323362
}

gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPService.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
2323
import org.apache.knox.gateway.services.Service;
2424
import org.apache.knox.gateway.services.ServiceLifecycleException;
25+
import org.apache.knox.gateway.services.security.AliasService;
2526

2627
import java.util.List;
2728
import java.util.Map;
@@ -34,6 +35,7 @@ public class KnoxLDAPService implements Service, GatewayConfigChangeListener {
3435
private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class);
3536

3637
KnoxLDAPServerManager ldapServerManager;
38+
AliasService aliasService;
3739
private boolean enabled;
3840

3941
@Override
@@ -46,13 +48,17 @@ public void init(GatewayConfig config, Map<String, String> options) throws Servi
4648

4749
try {
4850
// Initialize the LDAP server manager with configuration
49-
ldapServerManager = new KnoxLDAPServerManager();
51+
ldapServerManager = new KnoxLDAPServerManager(aliasService);
5052
ldapServerManager.initialize(config);
5153
} catch (Exception e) {
5254
throw new ServiceLifecycleException("Failed to initialize LDAP service", e);
5355
}
5456
}
5557

58+
public void setAliasService(AliasService aliasService) {
59+
this.aliasService = aliasService;
60+
}
61+
5662
@Override
5763
public void start() throws ServiceLifecycleException {
5864
if (!enabled) {
@@ -89,7 +95,7 @@ public void onGatewayConfigChanged(GatewayConfig config) {
8995
this.enabled = config.isLDAPEnabled();
9096

9197
if (this.enabled) {
92-
this.ldapServerManager = this.ldapServerManager == null ? new KnoxLDAPServerManager() : this.ldapServerManager;
98+
this.ldapServerManager = this.ldapServerManager == null ? new KnoxLDAPServerManager(aliasService) : this.ldapServerManager;
9399
ldapServerManager.stop();
94100
ldapServerManager.initialize(config);
95101
ldapServerManager.start();

gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapMessages.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ public interface LdapMessages {
3333
text = "LDAP service started successfully on port {0}")
3434
void ldapServiceStarted(int port);
3535

36+
@Message(level = MessageLevel.INFO,
37+
text = "Anonymous access disabled; clients must bind as: {0}")
38+
void ldapBindUserConfigured(String bindDn);
39+
3640
@Message(level = MessageLevel.INFO,
3741
text = "Stopping LDAP service on port {0}")
3842
void ldapServiceStopping(int port);

gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,16 @@
1818
package org.apache.knox.gateway.services.ldap;
1919

2020
import org.apache.directory.api.ldap.codec.api.ControlFactory;
21+
import org.apache.directory.api.ldap.model.cursor.EntryCursor;
22+
import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException;
23+
import org.apache.directory.api.ldap.model.exception.LdapException;
2124
import org.apache.directory.api.ldap.model.message.Control;
25+
import org.apache.directory.api.ldap.model.message.SearchScope;
26+
import org.apache.directory.ldap.client.api.LdapConnection;
27+
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
2228
import org.apache.directory.server.core.api.interceptor.Interceptor;
2329
import org.apache.knox.gateway.config.GatewayConfig;
30+
import org.apache.knox.gateway.services.security.AliasService;
2431
import org.apache.knox.gateway.services.ldap.control.RolesLookupBypassControlFactory;
2532
import org.apache.knox.gateway.services.ldap.model.constants.SchemaConstants;
2633
import org.easymock.EasyMock;
@@ -51,14 +58,20 @@
5158
*/
5259
public class KnoxLDAPServerManagerTest {
5360

61+
private static final String BIND_DN = "uid=knox,ou=system";
62+
private static final String BIND_PASSWORD = "knox-password";
63+
5464
private KnoxLDAPServerManager serverManager;
5565
private File tempWorkDir;
5666
private File tempLdapFile;
5767
private int port;
5868

5969
@Before
6070
public void setUp() throws Exception {
61-
serverManager = new KnoxLDAPServerManager();
71+
// By default no bind password is stored in the credential store, so the server
72+
// runs with anonymous access (the historical behavior). Bind-enforcing tests
73+
// rebuild the manager via useBindPassword(...).
74+
serverManager = new KnoxLDAPServerManager(aliasServiceReturning(null));
6275

6376
// Create temporary work directory
6477
tempWorkDir = File.createTempFile("knox-ldap-work", "");
@@ -395,6 +408,95 @@ public void testStartRegistersRolesLookupBypassControl() throws Exception {
395408
assertTrue(controlFactoryMap.get(SchemaConstants.ROLES_LOOKUP_BYPASS_CONTROL_OID) instanceof RolesLookupBypassControlFactory);
396409
}
397410

411+
@Test(expected = LdapException.class)
412+
public void testBindRequiredRejectsAnonymous() throws Exception {
413+
useBindPassword(BIND_PASSWORD);
414+
serverManager.initialize(createBindEnabledConfig());
415+
serverManager.start();
416+
417+
// Anonymous access is disabled, so an anonymous bind must be rejected.
418+
try (LdapConnection connection = new LdapNetworkConnection("localhost", port)) {
419+
connection.bind();
420+
}
421+
}
422+
423+
@Test
424+
public void testBindWithConfiguredCredentialsSucceeds() throws Exception {
425+
useBindPassword(BIND_PASSWORD);
426+
serverManager.initialize(createBindEnabledConfig());
427+
serverManager.start();
428+
429+
try (LdapConnection connection = new LdapNetworkConnection("localhost", port)) {
430+
connection.bind(BIND_DN, BIND_PASSWORD);
431+
assertTrue("Connection should be authenticated", connection.isAuthenticated());
432+
// An authenticated client should be able to search.
433+
try (EntryCursor cursor = connection.search("dc=test,dc=com", "(objectClass=*)", SearchScope.SUBTREE)) {
434+
assertTrue("Authenticated search should return at least one entry", cursor.next());
435+
}
436+
}
437+
}
438+
439+
@Test(expected = LdapAuthenticationException.class)
440+
public void testWrongBindPasswordRejected() throws Exception {
441+
useBindPassword(BIND_PASSWORD);
442+
serverManager.initialize(createBindEnabledConfig());
443+
serverManager.start();
444+
445+
try (LdapConnection connection = new LdapNetworkConnection("localhost", port)) {
446+
connection.bind(BIND_DN, "wrong-password");
447+
}
448+
}
449+
450+
@Test
451+
public void testAnonymousStillAllowedWhenUnconfigured() throws Exception {
452+
GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
453+
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
454+
expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
455+
expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
456+
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes();
457+
expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
458+
expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(createFileBackendInterceptorConfig()).anyTimes();
459+
replay(mockConfig);
460+
461+
serverManager.initialize(mockConfig);
462+
serverManager.start();
463+
464+
// No bind credentials configured -> anonymous access remains allowed (backward compatible).
465+
try (LdapConnection connection = new LdapNetworkConnection("localhost", port)) {
466+
connection.bind();
467+
try (EntryCursor cursor = connection.search("dc=test,dc=com", "(objectClass=*)", SearchScope.SUBTREE)) {
468+
assertTrue("Anonymous search should return at least one entry", cursor.next());
469+
}
470+
}
471+
}
472+
473+
private GatewayConfig createBindEnabledConfig() {
474+
GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
475+
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
476+
expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
477+
expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
478+
expect(mockConfig.getLDAPBindUser()).andReturn(BIND_DN).anyTimes();
479+
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes();
480+
expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
481+
expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(createFileBackendInterceptorConfig()).anyTimes();
482+
replay(mockConfig);
483+
return mockConfig;
484+
}
485+
486+
/** Rebuild the server manager so its credential store resolves the bind password to the given value. */
487+
private void useBindPassword(String password) throws Exception {
488+
serverManager = new KnoxLDAPServerManager(aliasServiceReturning(password));
489+
}
490+
491+
/** Create an AliasService whose gateway password lookups return the given value (null => alias not set). */
492+
private AliasService aliasServiceReturning(String password) throws Exception {
493+
AliasService aliasService = EasyMock.createNiceMock(AliasService.class);
494+
expect(aliasService.getPasswordFromAliasForGateway(EasyMock.anyString()))
495+
.andReturn(password == null ? null : password.toCharArray()).anyTimes();
496+
replay(aliasService);
497+
return aliasService;
498+
}
499+
398500
private Map<String, String> createFileBackendInterceptorConfig() {
399501
Map<String, String> config = new HashMap<>();
400502
config.put("interceptorType", "backend");

gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServiceTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import org.apache.knox.gateway.config.GatewayConfig;
2121
import org.apache.knox.gateway.services.ServiceLifecycleException;
22+
import org.apache.knox.gateway.services.security.AliasService;
2223
import org.junit.After;
2324
import org.junit.Before;
2425
import org.junit.Test;
@@ -29,6 +30,7 @@
2930
import java.util.Map;
3031

3132
import static org.easymock.EasyMock.createMock;
33+
import static org.easymock.EasyMock.createNiceMock;
3234
import static org.easymock.EasyMock.expect;
3335
import static org.easymock.EasyMock.replay;
3436
import static org.easymock.EasyMock.verify;
@@ -49,6 +51,10 @@ public class KnoxLDAPServiceTest {
4951
@Before
5052
public void setUp() throws Exception {
5153
ldapService = new KnoxLDAPService();
54+
// No bind password stored in the credential store -> anonymous access (default behavior).
55+
final AliasService aliasService = createNiceMock(AliasService.class);
56+
replay(aliasService);
57+
ldapService.setAliasService(aliasService);
5258
mockConfig = createMock(GatewayConfig.class);
5359

5460
// Create temporary directories and files
@@ -170,6 +176,7 @@ private void setupMockConfig(String backendType) throws Exception {
170176
expect(mockConfig.getGatewayDataDir()).andReturn(tempDataDir.getAbsolutePath()).atLeastOnce();
171177
expect(mockConfig.getLDAPPort()).andReturn(3890).times(1).andReturn(3891).anyTimes();
172178
expect(mockConfig.getLDAPBaseDN()).andReturn("file".equals(backendType) ? "dc=test,dc=com" : "dc=proxy,dc=com").atLeastOnce();
179+
expect(mockConfig.getLDAPBindUser()).andReturn(null).anyTimes();
173180
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("testbackend")).atLeastOnce();
174181
expect(mockConfig.getLDAPInterceptorConfig("testbackend")).andReturn(buildBackendConfig(backendType)).atLeastOnce();
175182
replay(mockConfig);

gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,6 +1245,11 @@ public String getLDAPBaseDN() {
12451245
return "dc=test,dc=com";
12461246
}
12471247

1248+
@Override
1249+
public String getLDAPBindUser() {
1250+
return null;
1251+
}
1252+
12481253
@Override
12491254
public List<String> getLDAPInterceptorNames() {
12501255
return List.of("testinterceptor");

gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ public interface GatewayConfig {
128128
String LDAP_ENABLED = "gateway.ldap.enabled";
129129
String LDAP_PORT = "gateway.ldap.port";
130130
String LDAP_BASE_DN = "gateway.ldap.base.dn";
131+
String LDAP_BIND_USER = "gateway.ldap.bind.user";
131132
String LDAP_INTERCEPTOR_NAMES = "gateway.ldap.interceptor.names";
132133
String LDAP_BACKEND_DATA_FILE = "gateway.ldap.backend.data.file";
133134
String LDAP_RECURSIVE_GROUP_RESOLUTION = "gateway.ldap.recursive.group.resolution";
@@ -1072,6 +1073,12 @@ public interface GatewayConfig {
10721073
*/
10731074
String getLDAPBaseDN();
10741075

1076+
/**
1077+
* @return the bind DN required to query the embedded LDAP service, or null/blank if
1078+
* anonymous access should be allowed
1079+
*/
1080+
String getLDAPBindUser();
1081+
10751082
/**
10761083
* @return the list of interceptor names for LDAP server
10771084
*/

0 commit comments

Comments
 (0)