Skip to content

Commit 1d02719

Browse files
committed
HBASE-29244 Support admin users acl setting with LDAP (Web UI only) (apache#6923)
- Exclude test files which do not work with hadoop-2 Signed-off-by: Nick Dimiduk <[email protected]> Reviewed-by: Dávid Paksy <[email protected]> (cherry picked from commit 06a74f1)
1 parent 1e1b2c8 commit 1d02719

File tree

7 files changed

+290
-94
lines changed

7 files changed

+290
-94
lines changed

hbase-http/pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,8 @@
408408
-->
409409
<testExcludes>
410410
<testExclude>**/org/apache/hadoop/hbase/http/TestLdapHttpServer**</testExclude>
411+
<testExclude>**/org/apache/hadoop/hbase/http/LdapServerTestBase**</testExclude>
412+
<testExclude>**/org/apache/hadoop/hbase/http/TestLdapAdminACL**/</testExclude>
411413
</testExcludes>
412414
</configuration>
413415
</plugin>

hbase-http/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ public class HttpServer implements FilterContainer {
148148
HTTP_SPNEGO_AUTHENTICATION_PREFIX + "admin.users";
149149
public static final String HTTP_SPNEGO_AUTHENTICATION_ADMIN_GROUPS_KEY =
150150
HTTP_SPNEGO_AUTHENTICATION_PREFIX + "admin.groups";
151+
152+
static final String HTTP_LDAP_AUTHENTICATION_PREFIX = HTTP_AUTHENTICATION_PREFIX + "ldap.";
153+
public static final String HTTP_LDAP_AUTHENTICATION_ADMIN_USERS_KEY =
154+
HTTP_LDAP_AUTHENTICATION_PREFIX + "admin.users";
155+
151156
public static final String HTTP_PRIVILEGED_CONF_KEY =
152157
"hbase.security.authentication.ui.config.protected";
153158
public static final String HTTP_UI_NO_CACHE_ENABLE_KEY = "hbase.http.filter.no-store.enable";

hbase-http/src/main/java/org/apache/hadoop/hbase/http/InfoServer.java

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,34 +77,51 @@ public InfoServer(String name, String bindAddress, int port, boolean findPort,
7777
c.get("ssl.server.truststore.type", "jks"));
7878
builder.excludeCiphers(c.get("ssl.server.exclude.cipher.list"));
7979
}
80+
81+
final String httpAuthType = c.get(HttpServer.HTTP_UI_AUTHENTICATION, "").toLowerCase();
8082
// Enable SPNEGO authentication
81-
if ("kerberos".equalsIgnoreCase(c.get(HttpServer.HTTP_UI_AUTHENTICATION, null))) {
83+
if ("kerberos".equals(httpAuthType)) {
8284
builder.setUsernameConfKey(HttpServer.HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_KEY)
8385
.setKeytabConfKey(HttpServer.HTTP_SPNEGO_AUTHENTICATION_KEYTAB_KEY)
8486
.setKerberosNameRulesKey(HttpServer.HTTP_SPNEGO_AUTHENTICATION_KRB_NAME_KEY)
8587
.setSignatureSecretFileKey(HttpServer.HTTP_AUTHENTICATION_SIGNATURE_SECRET_FILE_KEY)
8688
.setSecurityEnabled(true);
89+
}
8790

88-
// Set an admin ACL on sensitive webUI endpoints
91+
// Set an admin ACL on sensitive webUI endpoints (works only if SPNEGO or LDAP is enabled)
92+
if ("ldap".equals(httpAuthType) || "kerberos".equals(httpAuthType)) {
8993
AccessControlList acl = buildAdminAcl(c);
9094
builder.setACL(acl);
9195
}
96+
9297
this.httpServer = builder.build();
9398
}
9499

95100
/**
96101
* Builds an ACL that will restrict the users who can issue commands to endpoints on the UI which
97102
* are meant only for administrators.
98103
*/
99-
AccessControlList buildAdminAcl(Configuration conf) {
100-
final String userGroups = conf.get(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_USERS_KEY, null);
104+
static AccessControlList buildAdminAcl(Configuration conf) {
105+
// Initialize admin users based on whether http ui auth is set to ldap or kerberos
106+
String httpAuthType = conf.get(HttpServer.HTTP_UI_AUTHENTICATION, "").toLowerCase();
107+
final String adminUsers = getAdminUsers(conf, httpAuthType);
101108
final String adminGroups =
102109
conf.get(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_GROUPS_KEY, null);
103-
if (userGroups == null && adminGroups == null) {
110+
if (adminUsers == null && adminGroups == null) {
104111
// Backwards compatibility - if the user doesn't have anything set, allow all users in.
105112
return new AccessControlList("*", null);
106113
}
107-
return new AccessControlList(userGroups, adminGroups);
114+
return new AccessControlList(adminUsers, adminGroups);
115+
}
116+
117+
private static String getAdminUsers(Configuration conf, String httpAuthType) {
118+
if ("kerberos".equals(httpAuthType)) {
119+
return conf.get(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_USERS_KEY, null);
120+
} else if ("ldap".equals(httpAuthType)) {
121+
return conf.get(HttpServer.HTTP_LDAP_AUTHENTICATION_ADMIN_USERS_KEY, null);
122+
}
123+
// If the auth type is not kerberos or ldap, return null
124+
return null;
108125
}
109126

110127
/**
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hadoop.hbase.http;
19+
20+
import java.io.IOException;
21+
import java.net.HttpURLConnection;
22+
import java.net.URL;
23+
import org.apache.commons.codec.binary.Base64;
24+
import org.apache.directory.server.core.integ.CreateLdapServerRule;
25+
import org.apache.hadoop.conf.Configuration;
26+
import org.apache.hadoop.hbase.http.resource.JerseyResource;
27+
import org.junit.AfterClass;
28+
import org.junit.BeforeClass;
29+
import org.junit.ClassRule;
30+
import org.slf4j.Logger;
31+
import org.slf4j.LoggerFactory;
32+
33+
/**
34+
* Base class for setting up and testing an HTTP server with LDAP authentication.
35+
*/
36+
public class LdapServerTestBase extends HttpServerFunctionalTest {
37+
private static final Logger LOG = LoggerFactory.getLogger(LdapServerTestBase.class);
38+
39+
@ClassRule
40+
public static CreateLdapServerRule ldapRule = new CreateLdapServerRule();
41+
42+
protected static HttpServer server;
43+
protected static URL baseUrl;
44+
45+
private static final String AUTH_TYPE = "Basic ";
46+
47+
/**
48+
* Sets up the HTTP server with LDAP authentication before any tests are run.
49+
* @throws Exception if an error occurs during server setup
50+
*/
51+
@BeforeClass
52+
public static void setupServer() throws Exception {
53+
Configuration conf = new Configuration();
54+
setLdapConfigurations(conf);
55+
56+
server = createTestServer(conf);
57+
server.addUnprivilegedServlet("echo", "/echo", TestHttpServer.EchoServlet.class);
58+
server.addJerseyResourcePackage(JerseyResource.class.getPackage().getName(), "/jersey/*");
59+
server.start();
60+
61+
baseUrl = getServerURL(server);
62+
LOG.info("HTTP server started: " + baseUrl);
63+
}
64+
65+
/**
66+
* Stops the HTTP server after all tests are completed.
67+
* @throws Exception if an error occurs during server shutdown
68+
*/
69+
@AfterClass
70+
public static void stopServer() throws Exception {
71+
try {
72+
if (null != server) {
73+
server.stop();
74+
}
75+
} catch (Exception e) {
76+
LOG.info("Failed to stop info server", e);
77+
}
78+
}
79+
80+
/**
81+
* Configures the provided Configuration object for LDAP authentication.
82+
* @param conf the Configuration object to set LDAP properties on
83+
* @return the configured Configuration object
84+
*/
85+
protected static void setLdapConfigurations(Configuration conf) {
86+
conf.setInt(HttpServer.HTTP_MAX_THREADS, TestHttpServer.MAX_THREADS);
87+
88+
// Enable LDAP (pre-req)
89+
conf.set(HttpServer.HTTP_UI_AUTHENTICATION, "ldap");
90+
conf.set(HttpServer.FILTER_INITIALIZERS_PROPERTY,
91+
"org.apache.hadoop.hbase.http.lib.AuthenticationFilterInitializer");
92+
conf.set("hadoop.http.authentication.type", "ldap");
93+
conf.set("hadoop.http.authentication.ldap.providerurl", String.format("ldap://%s:%s",
94+
LdapConstants.LDAP_SERVER_ADDR, ldapRule.getLdapServer().getPort()));
95+
conf.set("hadoop.http.authentication.ldap.enablestarttls", "false");
96+
conf.set("hadoop.http.authentication.ldap.basedn", LdapConstants.LDAP_BASE_DN);
97+
}
98+
99+
/**
100+
* Generates a Basic Authentication header from the provided credentials.
101+
* @param credentials the credentials to encode
102+
* @return the Basic Authentication header
103+
*/
104+
private String getBasicAuthHeader(String credentials) {
105+
return AUTH_TYPE + new Base64(0).encodeToString(credentials.getBytes());
106+
}
107+
108+
/**
109+
* Opens an HTTP connection to the specified endpoint with optional Basic Authentication.
110+
* @param endpoint the endpoint to connect to
111+
* @param credentials the credentials for Basic Authentication (optional)
112+
* @return the opened HttpURLConnection
113+
* @throws IOException if an error occurs while opening the connection
114+
*/
115+
protected HttpURLConnection openConnection(String endpoint, String credentials)
116+
throws IOException {
117+
URL url = new URL(getServerURL(server) + endpoint);
118+
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
119+
if (credentials != null) {
120+
conn.setRequestProperty("Authorization", getBasicAuthHeader(credentials));
121+
}
122+
return conn;
123+
}
124+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hadoop.hbase.http;
19+
20+
import static org.junit.Assert.assertEquals;
21+
22+
import java.io.IOException;
23+
import java.net.HttpURLConnection;
24+
import org.apache.directory.server.annotations.CreateLdapServer;
25+
import org.apache.directory.server.annotations.CreateTransport;
26+
import org.apache.directory.server.core.annotations.ApplyLdifs;
27+
import org.apache.directory.server.core.annotations.ContextEntry;
28+
import org.apache.directory.server.core.annotations.CreateDS;
29+
import org.apache.directory.server.core.annotations.CreatePartition;
30+
import org.apache.hadoop.conf.Configuration;
31+
import org.apache.hadoop.fs.CommonConfigurationKeys;
32+
import org.apache.hadoop.hbase.HBaseClassTestRule;
33+
import org.apache.hadoop.hbase.http.resource.JerseyResource;
34+
import org.apache.hadoop.hbase.testclassification.MiscTests;
35+
import org.apache.hadoop.hbase.testclassification.SmallTests;
36+
import org.junit.BeforeClass;
37+
import org.junit.ClassRule;
38+
import org.junit.Test;
39+
import org.junit.experimental.categories.Category;
40+
import org.slf4j.Logger;
41+
import org.slf4j.LoggerFactory;
42+
43+
/**
44+
* Test class for admin ACLs with LDAP authentication on the HttpServer.
45+
*/
46+
@Category({ MiscTests.class, SmallTests.class })
47+
@CreateLdapServer(
48+
transports = { @CreateTransport(protocol = "LDAP", address = LdapConstants.LDAP_SERVER_ADDR), })
49+
@CreateDS(name = "TestLdapAdminACL", allowAnonAccess = true,
50+
partitions = { @CreatePartition(name = "Test_Partition", suffix = LdapConstants.LDAP_BASE_DN,
51+
contextEntry = @ContextEntry(entryLdif = "dn: " + LdapConstants.LDAP_BASE_DN + " \n"
52+
+ "dc: example\n" + "objectClass: top\n" + "objectClass: domain\n\n")) })
53+
@ApplyLdifs({ "dn: uid=bjones," + LdapConstants.LDAP_BASE_DN, "cn: Bob Jones", "sn: Jones",
54+
"objectClass: inetOrgPerson", "uid: bjones", "userPassword: p@ssw0rd",
55+
56+
"dn: uid=jdoe," + LdapConstants.LDAP_BASE_DN, "cn: John Doe", "sn: Doe",
57+
"objectClass: inetOrgPerson", "uid: jdoe", "userPassword: secure123" })
58+
public class TestLdapAdminACL extends LdapServerTestBase {
59+
60+
@ClassRule
61+
public static final HBaseClassTestRule CLASS_RULE =
62+
HBaseClassTestRule.forClass(TestLdapAdminACL.class);
63+
private static final Logger LOG = LoggerFactory.getLogger(TestLdapAdminACL.class);
64+
65+
private static final String ADMIN_CREDENTIALS = "bjones:p@ssw0rd";
66+
private static final String NON_ADMIN_CREDENTIALS = "jdoe:secure123";
67+
private static final String WRONG_CREDENTIALS = "bjones:password";
68+
69+
@BeforeClass
70+
public static void setupServer() throws Exception {
71+
Configuration conf = new Configuration();
72+
setLdapConfigurationWithACLs(conf);
73+
74+
server = createTestServer(conf, InfoServer.buildAdminAcl(conf));
75+
server.addUnprivilegedServlet("echo", "/echo", TestHttpServer.EchoServlet.class);
76+
// we will reuse /jmx which is a privileged servlet
77+
server.addJerseyResourcePackage(JerseyResource.class.getPackage().getName(), "/jersey/*");
78+
server.start();
79+
80+
baseUrl = getServerURL(server);
81+
LOG.info("HTTP server started: " + baseUrl);
82+
}
83+
84+
private static void setLdapConfigurationWithACLs(Configuration conf) {
85+
setLdapConfigurations(conf);
86+
87+
// Enable LDAP admin ACL
88+
conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, true);
89+
conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_INSTRUMENTATION_REQUIRES_ADMIN, true);
90+
conf.set(HttpServer.HTTP_LDAP_AUTHENTICATION_ADMIN_USERS_KEY, "bjones");
91+
}
92+
93+
@Test
94+
public void testAdminAllowedUnprivilegedServletAccess() throws IOException {
95+
HttpURLConnection conn = openConnection("/echo?a=b", ADMIN_CREDENTIALS);
96+
assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode());
97+
}
98+
99+
@Test
100+
public void testAdminAllowedPrivilegedServletAccess() throws IOException {
101+
HttpURLConnection conn = openConnection("/jmx", ADMIN_CREDENTIALS);
102+
assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode());
103+
}
104+
105+
@Test
106+
public void testNonAdminAllowedUnprivilegedServletAccess() throws IOException {
107+
HttpURLConnection conn = openConnection("/echo?a=b", NON_ADMIN_CREDENTIALS);
108+
assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode());
109+
}
110+
111+
@Test
112+
public void testNonAdminDisallowedPrivilegedServletAccess() throws IOException {
113+
HttpURLConnection conn = openConnection("/jmx", NON_ADMIN_CREDENTIALS);
114+
assertEquals(HttpURLConnection.HTTP_FORBIDDEN, conn.getResponseCode());
115+
}
116+
117+
@Test
118+
public void testWrongAuthDisallowedUnprivilegedServletAccess() throws IOException {
119+
HttpURLConnection conn = openConnection("/echo?a=b", WRONG_CREDENTIALS);
120+
assertEquals(HttpURLConnection.HTTP_FORBIDDEN, conn.getResponseCode());
121+
}
122+
123+
@Test
124+
public void testWrongAuthDisallowedPrivilegedServletAccess() throws IOException {
125+
HttpURLConnection conn = openConnection("/jmx", WRONG_CREDENTIALS);
126+
assertEquals(HttpURLConnection.HTTP_FORBIDDEN, conn.getResponseCode());
127+
}
128+
}

0 commit comments

Comments
 (0)