Skip to content

Commit 14c8a3f

Browse files
author
Vladimir Kotal
authored
LdapFacade to verify LDAP servers (#3198)
fixes #3196
1 parent f3f79db commit 14c8a3f

File tree

4 files changed

+236
-14
lines changed

4 files changed

+236
-14
lines changed

plugins/src/main/java/opengrok/auth/plugin/ldap/LdapFacade.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public class LdapFacade extends AbstractLdapProvider {
100100
private WebHooks webHooks;
101101

102102
private SearchControls controls;
103-
private int actualServer = 0;
103+
private int actualServer = -1;
104104
private long errorTimestamp = 0;
105105
private boolean reported = false;
106106

@@ -187,14 +187,13 @@ private void setWebHooks(WebHooks webHooks) {
187187
}
188188

189189
/**
190-
* Finds first working server in the pool.
190+
* Go through all servers in the pool and record the first working.
191191
*/
192-
private void prepareServers() {
192+
void prepareServers() {
193193
for (int i = 0; i < servers.size(); i++) {
194194
LdapServer server = servers.get(i);
195-
if (server.isWorking()) {
195+
if (server.isWorking() && actualServer == -1) {
196196
actualServer = i;
197-
return;
198197
}
199198
}
200199
}
@@ -245,7 +244,7 @@ public void setSearchBase(String base) {
245244

246245
@Override
247246
public boolean isConfigured() {
248-
return servers != null && servers.size() > 0 && LDAP_FILTER != null && searchBase != null;
247+
return servers != null && servers.size() > 0 && LDAP_FILTER != null && searchBase != null && actualServer != -1;
249248
}
250249

251250
/**

plugins/src/main/java/opengrok/auth/plugin/ldap/LdapServer.java

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,14 @@
2222
*/
2323
package opengrok.auth.plugin.ldap;
2424

25+
import java.io.IOException;
2526
import java.io.Serializable;
27+
import java.net.InetAddress;
28+
import java.net.InetSocketAddress;
29+
import java.net.Socket;
30+
import java.net.URI;
31+
import java.net.URISyntaxException;
32+
import java.net.UnknownHostException;
2633
import java.util.Hashtable;
2734
import java.util.logging.Level;
2835
import java.util.logging.Logger;
@@ -64,12 +71,12 @@ public LdapServer() {
6471

6572
public LdapServer(String server) {
6673
this(prepareEnv());
67-
this.url = server;
74+
setName(server);
6875
}
6976

7077
public LdapServer(String server, String username, String password) {
7178
this(prepareEnv());
72-
this.url = server;
79+
setName(server);
7380
this.username = username;
7481
this.password = password;
7582
}
@@ -122,13 +129,95 @@ public void setInterval(int interval) {
122129
this.interval = interval;
123130
}
124131

132+
private String urlToHostname(String urlStr) throws URISyntaxException {
133+
URI uri = new URI(urlStr);
134+
return uri.getHost();
135+
}
136+
137+
/**
138+
* This method converts the scheme from URI to port number.
139+
* It is limited to the ldap/ldaps schemes.
140+
* The method could be static however then it cannot be easily mocked in testing.
141+
* @return port number or -1 if the scheme in given URI is not known
142+
* @throws URISyntaxException if the URI is not valid
143+
*/
144+
public int getPort() throws URISyntaxException {
145+
URI uri = new URI(getUrl());
146+
switch (uri.getScheme()) {
147+
case "ldaps":
148+
return 636;
149+
case "ldap":
150+
return 389;
151+
}
152+
153+
return -1;
154+
}
155+
156+
private boolean isReachable(InetAddress addr, int port, int timeOutMillis) {
157+
try {
158+
try (Socket soc = new Socket()) {
159+
soc.connect(new InetSocketAddress(addr, port), timeOutMillis);
160+
}
161+
return true;
162+
} catch (IOException e) {
163+
return false;
164+
}
165+
}
166+
167+
/**
168+
* Wraps InetAddress.getAllByName() so that it can be mocked in testing.
169+
* (mocking static methods is not really possible with Mockito)
170+
* @param hostname hostname string
171+
* @return array of InetAddress objects
172+
* @throws UnknownHostException if the host cannot be resolved to any IP address
173+
*/
174+
public InetAddress[] getAddresses(String hostname) throws UnknownHostException {
175+
return InetAddress.getAllByName(hostname);
176+
}
177+
125178
/**
126-
* The LDAP server is working only when its connection is not null. This
127-
* tries to establish the connection if it is not established already.
179+
* Go through all IP addresses and find out if they are reachable.
180+
* @return true if all IP addresses are reachable, false otherwise
181+
*/
182+
public boolean isReachable() {
183+
try {
184+
InetAddress[] addresses = getAddresses(urlToHostname(getUrl()));
185+
if (addresses.length == 0) {
186+
LOGGER.log(Level.WARNING, "LDAP server {0} does not resolve to any IP address", this);
187+
return false;
188+
}
189+
190+
for (InetAddress addr : addresses) {
191+
// InetAddr.isReachable() is not sufficient as it can only check ICMP and TCP echo.
192+
int port = getPort();
193+
if (!isReachable(addr, port, getConnectTimeout())) {
194+
LOGGER.log(Level.WARNING, "LDAP server {0} is not reachable on {1}:{2}",
195+
new Object[]{this, addr, Integer.toString(port)});
196+
return false;
197+
}
198+
}
199+
} catch (UnknownHostException e) {
200+
LOGGER.log(Level.SEVERE, String.format("cannot get IP addresses for LDAP server %s", this), e);
201+
return false;
202+
} catch (URISyntaxException e) {
203+
LOGGER.log(Level.SEVERE, String.format("not a valid URI: %s", getUrl()), e);
204+
return false;
205+
}
206+
207+
return true;
208+
}
209+
210+
/**
211+
* The LDAP server is working only when it is reachable and its connection is not null.
212+
* This method tries to establish the connection if it is not established already.
128213
*
129214
* @return true if it is working
130215
*/
131216
public synchronized boolean isWorking() {
217+
if (!isReachable()) {
218+
return false;
219+
}
220+
132221
if (ctx == null) {
133222
ctx = connect();
134223
}
@@ -144,7 +233,7 @@ private synchronized LdapContext connect() {
144233
LOGGER.log(Level.INFO, "Connecting to LDAP server {0} ", this.toString());
145234

146235
if (errorTimestamp > 0 && errorTimestamp + interval > System.currentTimeMillis()) {
147-
LOGGER.log(Level.INFO, "LDAP server {0} is down", this.url);
236+
LOGGER.log(Level.WARNING, "LDAP server {0} is down", this.url);
148237
close();
149238
return null;
150239
}
@@ -169,7 +258,7 @@ private synchronized LdapContext connect() {
169258
LOGGER.log(Level.INFO, "Connected to LDAP server {0}", this.toString());
170259
errorTimestamp = 0;
171260
} catch (NamingException ex) {
172-
LOGGER.log(Level.INFO, "LDAP server {0} is not responding", env.get(Context.PROVIDER_URL));
261+
LOGGER.log(Level.WARNING, "LDAP server {0} is not responding", env.get(Context.PROVIDER_URL));
173262
errorTimestamp = System.currentTimeMillis();
174263
close();
175264
return ctx = null;
@@ -194,7 +283,7 @@ public NamingEnumeration<SearchResult> search(String name, String filter, Search
194283
}
195284

196285
/**
197-
* Lookups the LDAP server.
286+
* Perform LDAP search.
198287
*
199288
* @param name base dn for the search
200289
* @param filter LDAP filter
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package opengrok.auth.plugin;
2+
3+
import opengrok.auth.plugin.ldap.LdapServer;
4+
import org.junit.Test;
5+
import org.mockito.Mockito;
6+
7+
import java.io.IOException;
8+
import java.net.InetAddress;
9+
import java.net.ServerSocket;
10+
import java.net.Socket;
11+
import java.net.URISyntaxException;
12+
import java.net.UnknownHostException;
13+
14+
import static org.junit.jupiter.api.Assertions.*;
15+
import static org.mockito.ArgumentMatchers.any;
16+
import static org.mockito.Mockito.doReturn;
17+
18+
public class LdapServerTest {
19+
20+
@Test
21+
public void testInvalidURI() {
22+
LdapServer server = new LdapServer("foo:/\\/\\foo.bar");
23+
assertFalse(server.isReachable());
24+
}
25+
26+
@Test
27+
public void testGetPort() throws URISyntaxException {
28+
LdapServer server = new LdapServer("ldaps://foo.bar");
29+
assertEquals(636, server.getPort());
30+
31+
server = new LdapServer("ldap://foo.bar");
32+
assertEquals(389, server.getPort());
33+
34+
server = new LdapServer("crumble://foo.bar");
35+
assertEquals(-1, server.getPort());
36+
}
37+
38+
@Test
39+
public void testSetGetUsername() {
40+
LdapServer server = new LdapServer();
41+
42+
assertNull(server.getUsername());
43+
assertNull(server.getPassword());
44+
45+
final String testUsername = "foo";
46+
server.setUsername(testUsername);
47+
assertEquals(testUsername, server.getUsername());
48+
49+
final String testPassword = "bar";
50+
server.setPassword(testPassword);
51+
assertEquals(testPassword, server.getPassword());
52+
}
53+
54+
@Test
55+
public void testIsReachable() throws IOException, InterruptedException, URISyntaxException {
56+
// Start simple TCP server on port 6336. It has to be > 1024 to avoid BindException
57+
// due to permission denied.
58+
int testPort = 6336;
59+
InetAddress localhostAddr = InetAddress.getLocalHost();
60+
ServerSocket serverSocket = new ServerSocket(testPort, 1, localhostAddr);
61+
Thread thread = new Thread(() -> {
62+
try {
63+
while (true) {
64+
Socket client = serverSocket.accept();
65+
client.close();
66+
}
67+
} catch (IOException e) {
68+
e.printStackTrace();
69+
}
70+
});
71+
72+
thread.start();
73+
Socket socket = null;
74+
for (int i = 0; i < 3; i++) {
75+
try {
76+
socket = new Socket(localhostAddr, testPort);
77+
} catch (IOException e) {
78+
Thread.sleep(1000);
79+
}
80+
}
81+
82+
assertNotNull(socket);
83+
assertTrue(socket.isConnected());
84+
85+
// Mock getAddresses() to return single localhost IP address.
86+
LdapServer server = new LdapServer("ldaps://foo.bar.com");
87+
LdapServer serverSpy = Mockito.spy(server);
88+
Mockito.when(serverSpy.getAddresses(any())).thenReturn(new InetAddress[]{localhostAddr});
89+
doReturn(testPort).when(serverSpy).getPort();
90+
91+
// Test reachability.
92+
boolean reachable = serverSpy.isReachable();
93+
serverSocket.close();
94+
thread.join(5000);
95+
thread.interrupt();
96+
assertTrue(reachable);
97+
98+
// Test non-reachability.
99+
reachable = serverSpy.isReachable();
100+
assertFalse(reachable);
101+
}
102+
103+
@Test
104+
public void testEmptyAddressArray() throws UnknownHostException {
105+
LdapServer server = new LdapServer("ldaps://foo.bar.com");
106+
LdapServer serverSpy = Mockito.spy(server);
107+
Mockito.when(serverSpy.getAddresses(any())).thenReturn(new InetAddress[]{});
108+
assertFalse(serverSpy.isReachable());
109+
}
110+
}

plugins/src/test/java/opengrok/auth/plugin/LdapFacadeTest.java renamed to plugins/src/test/java/opengrok/auth/plugin/ldap/LdapFacadeTest.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1-
package opengrok.auth.plugin;
1+
package opengrok.auth.plugin.ldap;
22

33
import opengrok.auth.plugin.configuration.Configuration;
44
import opengrok.auth.plugin.ldap.LdapFacade;
55
import opengrok.auth.plugin.ldap.LdapServer;
66
import org.junit.Test;
7+
import org.mockito.Mockito;
78

89
import javax.naming.directory.SearchControls;
910

11+
import java.net.InetAddress;
12+
import java.net.UnknownHostException;
1013
import java.util.ArrayList;
1114
import java.util.Arrays;
1215
import java.util.Collections;
1316

1417
import static org.junit.Assert.assertEquals;
1518
import static org.junit.Assert.assertTrue;
19+
import static org.junit.jupiter.api.Assertions.assertFalse;
20+
import static org.mockito.ArgumentMatchers.any;
1621

1722
public class LdapFacadeTest {
1823
@Test
@@ -52,4 +57,23 @@ public void testToString() {
5257
assertEquals("{servers=http://foo.foo,http://bar.bar, searchBase=dc=foo,dc=com}",
5358
facade.toString());
5459
}
60+
61+
@Test
62+
public void testPrepareServersNegative() throws UnknownHostException {
63+
Configuration config = new Configuration();
64+
65+
LdapServer server1 = new LdapServer("ldap://foo.com");
66+
LdapServer serverSpy1 = Mockito.spy(server1);
67+
Mockito.when(serverSpy1.getAddresses(any())).thenReturn(new InetAddress[]{InetAddress.getLocalHost()});
68+
Mockito.when(serverSpy1.isReachable()).thenReturn(false);
69+
70+
LdapServer server2 = new LdapServer("ldap://bar.com");
71+
LdapServer serverSpy2 = Mockito.spy(server2);
72+
Mockito.when(serverSpy2.getAddresses(any())).thenReturn(new InetAddress[]{});
73+
74+
config.setServers(Arrays.asList(serverSpy1, serverSpy2));
75+
LdapFacade facade = new LdapFacade(config);
76+
facade.prepareServers();
77+
assertFalse(facade.isConfigured());
78+
}
5579
}

0 commit comments

Comments
 (0)