From 10da6eb4d73726e225a38a0f869b3f475896a6f8 Mon Sep 17 00:00:00 2001 From: Nihal Jain Date: Wed, 23 Apr 2025 13:02:25 +0530 Subject: [PATCH 1/4] HBASE-29244 Support admin users acl setting with LDAP (Web UI only) --- .../apache/hadoop/hbase/http/HttpServer.java | 5 + .../apache/hadoop/hbase/http/InfoServer.java | 24 ++- .../hadoop/hbase/http/TestLdapAdminACL.java | 173 ++++++++++++++++++ .../http/TestProxyUserSpnegoHttpServer.java | 17 +- src/main/asciidoc/_chapters/security.adoc | 44 ++++- 5 files changed, 240 insertions(+), 23 deletions(-) create mode 100644 hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapAdminACL.java diff --git a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java index d5af8df1c7fd..36a101b6ac73 100644 --- a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java +++ b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java @@ -148,6 +148,11 @@ public class HttpServer implements FilterContainer { HTTP_SPNEGO_AUTHENTICATION_PREFIX + "admin.users"; public static final String HTTP_SPNEGO_AUTHENTICATION_ADMIN_GROUPS_KEY = HTTP_SPNEGO_AUTHENTICATION_PREFIX + "admin.groups"; + + static final String HTTP_LDAP_AUTHENTICATION_PREFIX = HTTP_AUTHENTICATION_PREFIX + "ldap."; + public static final String HTTP_LDAP_AUTHENTICATION_ADMIN_USERS_KEY = + HTTP_LDAP_AUTHENTICATION_PREFIX + "admin.users"; + public static final String HTTP_PRIVILEGED_CONF_KEY = "hbase.security.authentication.ui.config.protected"; public static final String HTTP_UI_NO_CACHE_ENABLE_KEY = "hbase.http.filter.no-store.enable"; diff --git a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/InfoServer.java b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/InfoServer.java index c44222b83342..6557f4437719 100644 --- a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/InfoServer.java +++ b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/InfoServer.java @@ -77,18 +77,23 @@ public InfoServer(String name, String bindAddress, int port, boolean findPort, c.get("ssl.server.truststore.type", "jks")); builder.excludeCiphers(c.get("ssl.server.exclude.cipher.list")); } + + final String httpAuthType = c.get(HttpServer.HTTP_UI_AUTHENTICATION, "").toLowerCase(); // Enable SPNEGO authentication - if ("kerberos".equalsIgnoreCase(c.get(HttpServer.HTTP_UI_AUTHENTICATION, null))) { + if ("kerberos".equals(httpAuthType)) { builder.setUsernameConfKey(HttpServer.HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_KEY) .setKeytabConfKey(HttpServer.HTTP_SPNEGO_AUTHENTICATION_KEYTAB_KEY) .setKerberosNameRulesKey(HttpServer.HTTP_SPNEGO_AUTHENTICATION_KRB_NAME_KEY) .setSignatureSecretFileKey(HttpServer.HTTP_AUTHENTICATION_SIGNATURE_SECRET_FILE_KEY) .setSecurityEnabled(true); + } - // Set an admin ACL on sensitive webUI endpoints + // Set an admin ACL on sensitive webUI endpoints (works only if SPNEGO or LDAP is enabled) + if ("ldap".equals(httpAuthType) || "kerberos".equals(httpAuthType)) { AccessControlList acl = buildAdminAcl(c); builder.setACL(acl); } + this.httpServer = builder.build(); } @@ -96,15 +101,22 @@ public InfoServer(String name, String bindAddress, int port, boolean findPort, * Builds an ACL that will restrict the users who can issue commands to endpoints on the UI which * are meant only for administrators. */ - AccessControlList buildAdminAcl(Configuration conf) { - final String userGroups = conf.get(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_USERS_KEY, null); + static AccessControlList buildAdminAcl(Configuration conf) { + // Initialize admin users based on whether http ui auth is set to ldap or kerberos + String httpAuthType = conf.get(HttpServer.HTTP_UI_AUTHENTICATION, "").toLowerCase(); + String adminUsers = null; + if ("kerberos".equals(httpAuthType)) { + adminUsers = conf.get(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_USERS_KEY, null); + } else if ("ldap".equals(httpAuthType)) { + adminUsers = conf.get(HttpServer.HTTP_LDAP_AUTHENTICATION_ADMIN_USERS_KEY, null); + } final String adminGroups = conf.get(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_GROUPS_KEY, null); - if (userGroups == null && adminGroups == null) { + if (adminUsers == null && adminGroups == null) { // Backwards compatibility - if the user doesn't have anything set, allow all users in. return new AccessControlList("*", null); } - return new AccessControlList(userGroups, adminGroups); + return new AccessControlList(adminUsers, adminGroups); } /** diff --git a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapAdminACL.java b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapAdminACL.java new file mode 100644 index 000000000000..71097b616c90 --- /dev/null +++ b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapAdminACL.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.http; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import org.apache.commons.codec.binary.Base64; +import org.apache.directory.server.annotations.CreateLdapServer; +import org.apache.directory.server.annotations.CreateTransport; +import org.apache.directory.server.core.annotations.ApplyLdifs; +import org.apache.directory.server.core.annotations.ContextEntry; +import org.apache.directory.server.core.annotations.CreateDS; +import org.apache.directory.server.core.annotations.CreatePartition; +import org.apache.directory.server.core.integ.CreateLdapServerRule; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.CommonConfigurationKeys; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.http.resource.JerseyResource; +import org.apache.hadoop.hbase.testclassification.MiscTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test class for LDAP authentication on the HttpServer. + */ +@Category({ MiscTests.class, SmallTests.class }) +@CreateLdapServer( + transports = { @CreateTransport(protocol = "LDAP", address = LdapConstants.LDAP_SERVER_ADDR), }) +@CreateDS(allowAnonAccess = true, + partitions = { @CreatePartition(name = "Test_Partition", suffix = LdapConstants.LDAP_BASE_DN, + contextEntry = @ContextEntry(entryLdif = "dn: " + LdapConstants.LDAP_BASE_DN + " \n" + + "dc: example\n" + "objectClass: top\n" + "objectClass: domain\n\n")) }) +@ApplyLdifs({ "dn: uid=bjones," + LdapConstants.LDAP_BASE_DN, "cn: Bob Jones", "sn: Jones", + "objectClass: inetOrgPerson", "uid: bjones", "userPassword: p@ssw0rd", + + "dn: uid=jdoe," + LdapConstants.LDAP_BASE_DN, "cn: John Doe", "sn: Doe", + "objectClass: inetOrgPerson", "uid: jdoe", "userPassword: secure123" }) +public class TestLdapAdminACL extends HttpServerFunctionalTest { + + private static final String ADMIN_CREDENTIALS = "bjones:p@ssw0rd"; + private static final String NON_ADMIN_CREDENTIALS = "jdoe:secure123"; + private static final String WRONG_CREDENTIALS = "bjones:password"; + private static final String AUTH_TYPE = "Basic "; + + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestLdapAdminACL.class); + private static final Logger LOG = LoggerFactory.getLogger(TestLdapAdminACL.class); + @ClassRule + public static CreateLdapServerRule serverRule = new CreateLdapServerRule(); + private static HttpServer server; + private static URL baseUrl; + + @BeforeClass + public static void setupServer() throws Exception { + Configuration conf = new Configuration(); + buildLdapConfiguration(conf); + server = createTestServer(conf, InfoServer.buildAdminAcl(conf)); + server.addUnprivilegedServlet("echo", "/echo", TestHttpServer.EchoServlet.class); + // we will reuse /jmx which is a privileged servlet + server.addJerseyResourcePackage(JerseyResource.class.getPackage().getName(), "/jersey/*"); + server.start(); + + baseUrl = getServerURL(server); + + LOG.info("HTTP server started: " + baseUrl); + } + + @AfterClass + public static void stopServer() throws Exception { + try { + if (null != server) { + server.stop(); + } + } catch (Exception e) { + LOG.info("Failed to stop info server", e); + } + } + + private static Configuration buildLdapConfiguration(Configuration conf) { + + conf.setInt(HttpServer.HTTP_MAX_THREADS, TestHttpServer.MAX_THREADS); + + // Enable LDAP (pre-req) + conf.set(HttpServer.HTTP_UI_AUTHENTICATION, "ldap"); + conf.set(HttpServer.FILTER_INITIALIZERS_PROPERTY, + "org.apache.hadoop.hbase.http.lib.AuthenticationFilterInitializer"); + conf.set("hadoop.http.authentication.type", "ldap"); + conf.set("hadoop.http.authentication.ldap.providerurl", String.format("ldap://%s:%s", + LdapConstants.LDAP_SERVER_ADDR, serverRule.getLdapServer().getPort())); + conf.set("hadoop.http.authentication.ldap.enablestarttls", "false"); + conf.set("hadoop.http.authentication.ldap.basedn", LdapConstants.LDAP_BASE_DN); + conf.set("hadoop.http.authentication.ldap.admin.group", + "cn=admins,ou=groups," + LdapConstants.LDAP_BASE_DN); + + // Enable LDAP admin ACL + conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, true); + conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_INSTRUMENTATION_REQUIRES_ADMIN, true); + conf.set(HttpServer.HTTP_LDAP_AUTHENTICATION_ADMIN_USERS_KEY, "bjones"); + return conf; + } + + private String getBasicAuthHeader(String credentials) { + return AUTH_TYPE + new Base64(0).encodeToString(credentials.getBytes()); + } + + private HttpURLConnection openConnection(String endpoint, String credentials) throws IOException { + URL url = new URL(getServerURL(server) + endpoint); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestProperty("Authorization", getBasicAuthHeader(credentials)); + return conn; + } + + @Test + public void testAdminAllowedUnprivilegedServletAccess() throws IOException { + HttpURLConnection conn = openConnection("/echo?a=b", ADMIN_CREDENTIALS); + assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); + } + + @Test + public void testAdminAllowedPrivilegedServletAccess() throws IOException { + HttpURLConnection conn = openConnection("/jmx", ADMIN_CREDENTIALS); + assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); + } + + @Test + public void testNonAdminAllowedUnprivilegedServletAccess() throws IOException { + HttpURLConnection conn = openConnection("/echo?a=b", NON_ADMIN_CREDENTIALS); + assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); + } + + @Test + public void testNonAdminDisallowedPrivilegedServletAccess() throws IOException { + HttpURLConnection conn = openConnection("/jmx", NON_ADMIN_CREDENTIALS); + assertEquals(HttpURLConnection.HTTP_FORBIDDEN, conn.getResponseCode()); + } + + @Test + public void testWrongAuthDisallowedUnprivilegedServletAccess() throws IOException { + HttpURLConnection conn = openConnection("/echo?a=b", WRONG_CREDENTIALS); + assertEquals(HttpURLConnection.HTTP_FORBIDDEN, conn.getResponseCode()); + } + + @Test + public void testWrongAuthDisallowedPrivilegedServletAccess() throws IOException { + HttpURLConnection conn = openConnection("/jmx", WRONG_CREDENTIALS); + assertEquals(HttpURLConnection.HTTP_FORBIDDEN, conn.getResponseCode()); + } +} diff --git a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestProxyUserSpnegoHttpServer.java b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestProxyUserSpnegoHttpServer.java index 4b900dfe6de5..9c1caa1d16c2 100644 --- a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestProxyUserSpnegoHttpServer.java +++ b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestProxyUserSpnegoHttpServer.java @@ -126,7 +126,7 @@ public static void setupServer() throws Exception { setupUser(kdc, infoServerKeytab, serverPrincipal); buildSpnegoConfiguration(conf, serverPrincipal, infoServerKeytab); - AccessControlList acl = buildAdminAcl(conf); + AccessControlList acl = InfoServer.buildAdminAcl(conf); server = createTestServerWithSecurityAndAcl(conf, acl); server.addPrivilegedServlet("echo", "/echo", EchoServlet.class); @@ -182,21 +182,6 @@ protected static Configuration buildSpnegoConfiguration(Configuration conf, return conf; } - /** - * Builds an ACL that will restrict the users who can issue commands to endpoints on the UI which - * are meant only for administrators. - */ - public static AccessControlList buildAdminAcl(Configuration conf) { - final String userGroups = conf.get(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_USERS_KEY, null); - final String adminGroups = - conf.get(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_GROUPS_KEY, null); - if (userGroups == null && adminGroups == null) { - // Backwards compatibility - if the user doesn't have anything set, allow all users in. - return new AccessControlList("*", null); - } - return new AccessControlList(userGroups, adminGroups); - } - @Test public void testProxyAllowed() throws Exception { testProxy(WHEEL_PRINCIPAL, PRIVILEGED_PRINCIPAL, HttpURLConnection.HTTP_OK, null); diff --git a/src/main/asciidoc/_chapters/security.adoc b/src/main/asciidoc/_chapters/security.adoc index 07cbfa7cb948..63026e87d8a2 100644 --- a/src/main/asciidoc/_chapters/security.adoc +++ b/src/main/asciidoc/_chapters/security.adoc @@ -137,7 +137,7 @@ A number of properties exist to configure SPNEGO authentication for the web serv ---- -=== Defining administrators of the Web UI +=== Defining administrators of the Web UI with SPNEGO In the previous section, we cover how to enable authentication for the Web UI via SPNEGO. However, some portions of the Web UI could be used to impact the availability and performance @@ -255,7 +255,49 @@ A number of properties exist to configure LDAP authentication for the web server of Active Directory server. ---- +=== Defining Administrators of the Web UI with LDAP +In the previous section, we discussed enabling authentication for the Web UI via LDAP. Certain portions of the Web UI can impact the availability and performance of an HBase cluster. To safeguard these sensitive endpoints, it is essential to restrict access to authorized administrators only. + +HBase provides a mechanism to define administrators for the Web UI through a list of usernames in the `hbase-site.xml` configuration file. + +==== Configuration Example + +To specify the administrators, use the following property in `hbase-site.xml`: + +[source,xml] +---- + + hbase.security.authentication.ldap.admin.users + admin1,admin2,admin3 + +---- + +* `hbase.security.authentication.ldap.admin.users`: This property defines a comma-separated list of usernames that are mapped to LDAP identities. These users will have administrative access to sensitive Web UI endpoints. + +==== Mapping Usernames to LDAP Identities + +The usernames listed in the `hbase.security.authentication.ldap.admin.users` property must correspond to the LDAP identities as defined by the `binddomain` rules in `hbase-site.xml`. For example: + +[source,xml] +---- + + hadoop.http.authentication.ldap.binddomain + EXAMPLE.COM + +---- + +In this case, the LDAP identity `user@EXAMPLE.COM` would map to the username `user`. + +==== Notes + +* Ensure that the LDAP server is properly configured and running. +* Only users explicitly listed in the `hbase.security.authentication.ldap.admin.users` property will have access to sensitive endpoints. +* Non-administrative users can still access non-sensitive endpoints, provided they are authenticated. + +By defining administrators in this way, you can ensure that only authorized personnel can interact with critical Web UI functionalities, thereby enhancing the security and stability of your HBase cluster. + +--- === Other UI security-related configuration While it is a clear anti-pattern for HBase developers, the developers acknowledge that the HBase From ef0d4d40ee674160951d32d3e9d6b141e43e3068 Mon Sep 17 00:00:00 2001 From: Nihal Jain Date: Wed, 23 Apr 2025 13:45:26 +0530 Subject: [PATCH 2/4] HBASE-29244 Support admin users acl setting with LDAP (Web UI only) --- src/main/asciidoc/_chapters/security.adoc | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/main/asciidoc/_chapters/security.adoc b/src/main/asciidoc/_chapters/security.adoc index 63026e87d8a2..63a82459f7e5 100644 --- a/src/main/asciidoc/_chapters/security.adoc +++ b/src/main/asciidoc/_chapters/security.adoc @@ -261,8 +261,6 @@ In the previous section, we discussed enabling authentication for the Web UI via HBase provides a mechanism to define administrators for the Web UI through a list of usernames in the `hbase-site.xml` configuration file. -==== Configuration Example - To specify the administrators, use the following property in `hbase-site.xml`: [source,xml] @@ -273,25 +271,11 @@ To specify the administrators, use the following property in `hbase-site.xml`: ---- -* `hbase.security.authentication.ldap.admin.users`: This property defines a comma-separated list of usernames that are mapped to LDAP identities. These users will have administrative access to sensitive Web UI endpoints. - -==== Mapping Usernames to LDAP Identities - -The usernames listed in the `hbase.security.authentication.ldap.admin.users` property must correspond to the LDAP identities as defined by the `binddomain` rules in `hbase-site.xml`. For example: - -[source,xml] ----- - - hadoop.http.authentication.ldap.binddomain - EXAMPLE.COM - ----- - -In this case, the LDAP identity `user@EXAMPLE.COM` would map to the username `user`. +The usernames listed in the above property should correspond to the LDAP usernames of the administrators. ==== Notes -* Ensure that the LDAP server is properly configured and running. +* Ensure that the LDAP server is properly configured and running. See the previous section for details. * Only users explicitly listed in the `hbase.security.authentication.ldap.admin.users` property will have access to sensitive endpoints. * Non-administrative users can still access non-sensitive endpoints, provided they are authenticated. From 7dc8e627ab23b950c87c535a1344be91348fc13b Mon Sep 17 00:00:00 2001 From: Nihal Jain Date: Fri, 25 Apr 2025 03:36:46 +0530 Subject: [PATCH 3/4] Fix tests, cleanup code --- .../hadoop/hbase/http/LdapServerTestBase.java | 125 ++++++++++++++++++ .../hadoop/hbase/http/TestLdapAdminACL.java | 66 ++------- .../hadoop/hbase/http/TestLdapHttpServer.java | 79 +---------- 3 files changed, 143 insertions(+), 127 deletions(-) create mode 100644 hbase-http/src/test/java/org/apache/hadoop/hbase/http/LdapServerTestBase.java diff --git a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/LdapServerTestBase.java b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/LdapServerTestBase.java new file mode 100644 index 000000000000..4b599ebf35ae --- /dev/null +++ b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/LdapServerTestBase.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.http; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import org.apache.commons.codec.binary.Base64; +import org.apache.directory.server.core.integ.CreateLdapServerRule; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.http.resource.JerseyResource; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for setting up and testing an HTTP server with LDAP authentication. + */ +public class LdapServerTestBase extends HttpServerFunctionalTest { + private static final Logger LOG = LoggerFactory.getLogger(LdapServerTestBase.class); + + @ClassRule + public static CreateLdapServerRule ldapRule = new CreateLdapServerRule(); + + protected static HttpServer server; + protected static URL baseUrl; + + private static final String AUTH_TYPE = "Basic "; + + /** + * Sets up the HTTP server with LDAP authentication before any tests are run. + * @throws Exception if an error occurs during server setup + */ + @BeforeClass + public static void setupServer() throws Exception { + Configuration conf = new Configuration(); + setLdapConfigurations(conf); + + server = createTestServer(conf); + server.addUnprivilegedServlet("echo", "/echo", TestHttpServer.EchoServlet.class); + server.addJerseyResourcePackage(JerseyResource.class.getPackage().getName(), "/jersey/*"); + server.start(); + + baseUrl = getServerURL(server); + LOG.info("HTTP server started: " + baseUrl); + } + + /** + * Stops the HTTP server after all tests are completed. + * @throws Exception if an error occurs during server shutdown + */ + @AfterClass + public static void stopServer() throws Exception { + try { + if (null != server) { + server.stop(); + } + } catch (Exception e) { + LOG.info("Failed to stop info server", e); + } + } + + /** + * Configures the provided Configuration object for LDAP authentication. + * @param conf the Configuration object to set LDAP properties on + * @return the configured Configuration object + */ + protected static Configuration setLdapConfigurations(Configuration conf) { + conf.setInt(HttpServer.HTTP_MAX_THREADS, TestHttpServer.MAX_THREADS); + + // Enable LDAP (pre-req) + conf.set(HttpServer.HTTP_UI_AUTHENTICATION, "ldap"); + conf.set(HttpServer.FILTER_INITIALIZERS_PROPERTY, + "org.apache.hadoop.hbase.http.lib.AuthenticationFilterInitializer"); + conf.set("hadoop.http.authentication.type", "ldap"); + conf.set("hadoop.http.authentication.ldap.providerurl", String.format("ldap://%s:%s", + LdapConstants.LDAP_SERVER_ADDR, ldapRule.getLdapServer().getPort())); + conf.set("hadoop.http.authentication.ldap.enablestarttls", "false"); + conf.set("hadoop.http.authentication.ldap.basedn", LdapConstants.LDAP_BASE_DN); + return conf; + } + + /** + * Generates a Basic Authentication header from the provided credentials. + * @param credentials the credentials to encode + * @return the Basic Authentication header + */ + String getBasicAuthHeader(String credentials) { + return AUTH_TYPE + new Base64(0).encodeToString(credentials.getBytes()); + } + + /** + * Opens an HTTP connection to the specified endpoint with optional Basic Authentication. + * @param endpoint the endpoint to connect to + * @param credentials the credentials for Basic Authentication (optional) + * @return the opened HttpURLConnection + * @throws IOException if an error occurs while opening the connection + */ + protected HttpURLConnection openConnection(String endpoint, String credentials) + throws IOException { + URL url = new URL(getServerURL(server) + endpoint); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + if (credentials != null) { + conn.setRequestProperty("Authorization", getBasicAuthHeader(credentials)); + } + return conn; + } +} diff --git a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapAdminACL.java b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapAdminACL.java index 71097b616c90..b85282b332db 100644 --- a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapAdminACL.java +++ b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapAdminACL.java @@ -21,22 +21,18 @@ import java.io.IOException; import java.net.HttpURLConnection; -import java.net.URL; -import org.apache.commons.codec.binary.Base64; import org.apache.directory.server.annotations.CreateLdapServer; import org.apache.directory.server.annotations.CreateTransport; import org.apache.directory.server.core.annotations.ApplyLdifs; import org.apache.directory.server.core.annotations.ContextEntry; import org.apache.directory.server.core.annotations.CreateDS; import org.apache.directory.server.core.annotations.CreatePartition; -import org.apache.directory.server.core.integ.CreateLdapServerRule; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.CommonConfigurationKeys; import org.apache.hadoop.hbase.HBaseClassTestRule; import org.apache.hadoop.hbase.http.resource.JerseyResource; import org.apache.hadoop.hbase.testclassification.MiscTests; import org.apache.hadoop.hbase.testclassification.SmallTests; -import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; @@ -45,12 +41,12 @@ import org.slf4j.LoggerFactory; /** - * Test class for LDAP authentication on the HttpServer. + * Test class for admin ACLs with LDAP authentication on the HttpServer. */ @Category({ MiscTests.class, SmallTests.class }) @CreateLdapServer( transports = { @CreateTransport(protocol = "LDAP", address = LdapConstants.LDAP_SERVER_ADDR), }) -@CreateDS(allowAnonAccess = true, +@CreateDS(name = "TestLdapAdminACL", allowAnonAccess = true, partitions = { @CreatePartition(name = "Test_Partition", suffix = LdapConstants.LDAP_BASE_DN, contextEntry = @ContextEntry(entryLdif = "dn: " + LdapConstants.LDAP_BASE_DN + " \n" + "dc: example\n" + "objectClass: top\n" + "objectClass: domain\n\n")) }) @@ -59,26 +55,22 @@ "dn: uid=jdoe," + LdapConstants.LDAP_BASE_DN, "cn: John Doe", "sn: Doe", "objectClass: inetOrgPerson", "uid: jdoe", "userPassword: secure123" }) -public class TestLdapAdminACL extends HttpServerFunctionalTest { - - private static final String ADMIN_CREDENTIALS = "bjones:p@ssw0rd"; - private static final String NON_ADMIN_CREDENTIALS = "jdoe:secure123"; - private static final String WRONG_CREDENTIALS = "bjones:password"; - private static final String AUTH_TYPE = "Basic "; +public class TestLdapAdminACL extends LdapServerTestBase { @ClassRule public static final HBaseClassTestRule CLASS_RULE = HBaseClassTestRule.forClass(TestLdapAdminACL.class); private static final Logger LOG = LoggerFactory.getLogger(TestLdapAdminACL.class); - @ClassRule - public static CreateLdapServerRule serverRule = new CreateLdapServerRule(); - private static HttpServer server; - private static URL baseUrl; + + private static final String ADMIN_CREDENTIALS = "bjones:p@ssw0rd"; + private static final String NON_ADMIN_CREDENTIALS = "jdoe:secure123"; + private static final String WRONG_CREDENTIALS = "bjones:password"; @BeforeClass public static void setupServer() throws Exception { Configuration conf = new Configuration(); - buildLdapConfiguration(conf); + setLdapConfigurationWithACLs(conf); + server = createTestServer(conf, InfoServer.buildAdminAcl(conf)); server.addUnprivilegedServlet("echo", "/echo", TestHttpServer.EchoServlet.class); // we will reuse /jmx which is a privileged servlet @@ -86,36 +78,11 @@ public static void setupServer() throws Exception { server.start(); baseUrl = getServerURL(server); - LOG.info("HTTP server started: " + baseUrl); } - @AfterClass - public static void stopServer() throws Exception { - try { - if (null != server) { - server.stop(); - } - } catch (Exception e) { - LOG.info("Failed to stop info server", e); - } - } - - private static Configuration buildLdapConfiguration(Configuration conf) { - - conf.setInt(HttpServer.HTTP_MAX_THREADS, TestHttpServer.MAX_THREADS); - - // Enable LDAP (pre-req) - conf.set(HttpServer.HTTP_UI_AUTHENTICATION, "ldap"); - conf.set(HttpServer.FILTER_INITIALIZERS_PROPERTY, - "org.apache.hadoop.hbase.http.lib.AuthenticationFilterInitializer"); - conf.set("hadoop.http.authentication.type", "ldap"); - conf.set("hadoop.http.authentication.ldap.providerurl", String.format("ldap://%s:%s", - LdapConstants.LDAP_SERVER_ADDR, serverRule.getLdapServer().getPort())); - conf.set("hadoop.http.authentication.ldap.enablestarttls", "false"); - conf.set("hadoop.http.authentication.ldap.basedn", LdapConstants.LDAP_BASE_DN); - conf.set("hadoop.http.authentication.ldap.admin.group", - "cn=admins,ou=groups," + LdapConstants.LDAP_BASE_DN); + private static Configuration setLdapConfigurationWithACLs(Configuration conf) { + setLdapConfigurations(conf); // Enable LDAP admin ACL conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, true); @@ -124,17 +91,6 @@ private static Configuration buildLdapConfiguration(Configuration conf) { return conf; } - private String getBasicAuthHeader(String credentials) { - return AUTH_TYPE + new Base64(0).encodeToString(credentials.getBytes()); - } - - private HttpURLConnection openConnection(String endpoint, String credentials) throws IOException { - URL url = new URL(getServerURL(server) + endpoint); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestProperty("Authorization", getBasicAuthHeader(credentials)); - return conn; - } - @Test public void testAdminAllowedUnprivilegedServletAccess() throws IOException { HttpURLConnection conn = openConnection("/echo?a=b", ADMIN_CREDENTIALS); diff --git a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapHttpServer.java b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapHttpServer.java index 8bb48d50753e..bff4dc9d9591 100644 --- a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapHttpServer.java +++ b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapHttpServer.java @@ -21,27 +21,18 @@ import java.io.IOException; import java.net.HttpURLConnection; -import java.net.URL; -import org.apache.commons.codec.binary.Base64; import org.apache.directory.server.annotations.CreateLdapServer; import org.apache.directory.server.annotations.CreateTransport; import org.apache.directory.server.core.annotations.ApplyLdifs; import org.apache.directory.server.core.annotations.ContextEntry; import org.apache.directory.server.core.annotations.CreateDS; import org.apache.directory.server.core.annotations.CreatePartition; -import org.apache.directory.server.core.integ.CreateLdapServerRule; -import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.HBaseClassTestRule; -import org.apache.hadoop.hbase.http.resource.JerseyResource; import org.apache.hadoop.hbase.testclassification.MiscTests; import org.apache.hadoop.hbase.testclassification.SmallTests; -import org.junit.AfterClass; -import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Test class for LDAP authentication on the HttpServer. @@ -49,92 +40,36 @@ @Category({ MiscTests.class, SmallTests.class }) @CreateLdapServer( transports = { @CreateTransport(protocol = "LDAP", address = LdapConstants.LDAP_SERVER_ADDR), }) -@CreateDS(allowAnonAccess = true, +@CreateDS(name = "TestLdapHttpServer", allowAnonAccess = true, partitions = { @CreatePartition(name = "Test_Partition", suffix = LdapConstants.LDAP_BASE_DN, contextEntry = @ContextEntry(entryLdif = "dn: " + LdapConstants.LDAP_BASE_DN + " \n" + "dc: example\n" + "objectClass: top\n" + "objectClass: domain\n\n")) }) @ApplyLdifs({ "dn: uid=bjones," + LdapConstants.LDAP_BASE_DN, "cn: Bob Jones", "sn: Jones", "objectClass: inetOrgPerson", "uid: bjones", "userPassword: p@ssw0rd" }) -public class TestLdapHttpServer extends HttpServerFunctionalTest { +public class TestLdapHttpServer extends LdapServerTestBase { @ClassRule public static final HBaseClassTestRule CLASS_RULE = HBaseClassTestRule.forClass(TestLdapHttpServer.class); - @ClassRule - public static CreateLdapServerRule serverRule = new CreateLdapServerRule(); - - private static final Logger LOG = LoggerFactory.getLogger(TestLdapHttpServer.class); - - private static HttpServer server; - private static URL baseUrl; - - @BeforeClass - public static void setupServer() throws Exception { - Configuration conf = new Configuration(); - buildLdapConfiguration(conf); - server = createTestServer(conf); - server.addUnprivilegedServlet("echo", "/echo", TestHttpServer.EchoServlet.class); - server.addJerseyResourcePackage(JerseyResource.class.getPackage().getName(), "/jersey/*"); - server.start(); - baseUrl = getServerURL(server); - - LOG.info("HTTP server started: " + baseUrl); - } - @AfterClass - public static void stopServer() throws Exception { - try { - if (null != server) { - server.stop(); - } - } catch (Exception e) { - LOG.info("Failed to stop info server", e); - } - } - - private static Configuration buildLdapConfiguration(Configuration conf) { - - conf.setInt(HttpServer.HTTP_MAX_THREADS, TestHttpServer.MAX_THREADS); - - // Enable LDAP (pre-req) - conf.set(HttpServer.HTTP_UI_AUTHENTICATION, "ldap"); - conf.set(HttpServer.FILTER_INITIALIZERS_PROPERTY, - "org.apache.hadoop.hbase.http.lib.AuthenticationFilterInitializer"); - conf.set("hadoop.http.authentication.type", "ldap"); - conf.set("hadoop.http.authentication.ldap.providerurl", String.format("ldap://%s:%s", - LdapConstants.LDAP_SERVER_ADDR, serverRule.getLdapServer().getPort())); - conf.set("hadoop.http.authentication.ldap.enablestarttls", "false"); - conf.set("hadoop.http.authentication.ldap.basedn", LdapConstants.LDAP_BASE_DN); - return conf; - } + private static final String BJONES_CREDENTIALS = "bjones:p@ssw0rd"; + private static final String WRONG_CREDENTIALS = "bjones:password"; @Test public void testUnauthorizedClientsDisallowed() throws IOException { - URL url = new URL(getServerURL(server), "/echo?a=b"); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + HttpURLConnection conn = openConnection("/echo?a=b", null); assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, conn.getResponseCode()); } @Test public void testAllowedClient() throws IOException { - URL url = new URL(getServerURL(server), "/echo?a=b"); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - final Base64 base64 = new Base64(0); - String userCredentials = "bjones:p@ssw0rd"; - String basicAuth = "Basic " + base64.encodeToString(userCredentials.getBytes()); - conn.setRequestProperty("Authorization", basicAuth); + HttpURLConnection conn = openConnection("/echo?a=b", BJONES_CREDENTIALS); assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); } @Test public void testWrongAuthClientsDisallowed() throws IOException { - URL url = new URL(getServerURL(server), "/echo?a=b"); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - final Base64 base64 = new Base64(0); - String userCredentials = "bjones:password"; - String basicAuth = "Basic " + base64.encodeToString(userCredentials.getBytes()); - conn.setRequestProperty("Authorization", basicAuth); + HttpURLConnection conn = openConnection("/echo?a=b", WRONG_CREDENTIALS); assertEquals(HttpURLConnection.HTTP_FORBIDDEN, conn.getResponseCode()); } - } From 3e2891b8a15fad7eb92ba295d479f9a45f19d0ab Mon Sep 17 00:00:00 2001 From: Nihal Jain Date: Mon, 19 May 2025 15:12:27 +0530 Subject: [PATCH 4/4] * Address review comments --- .../apache/hadoop/hbase/http/InfoServer.java | 17 +++++++++++------ .../hadoop/hbase/http/LdapServerTestBase.java | 5 ++--- .../hadoop/hbase/http/TestLdapAdminACL.java | 3 +-- src/main/asciidoc/_chapters/security.adoc | 1 + 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/InfoServer.java b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/InfoServer.java index 6557f4437719..aa25ef427629 100644 --- a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/InfoServer.java +++ b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/InfoServer.java @@ -104,12 +104,7 @@ public InfoServer(String name, String bindAddress, int port, boolean findPort, static AccessControlList buildAdminAcl(Configuration conf) { // Initialize admin users based on whether http ui auth is set to ldap or kerberos String httpAuthType = conf.get(HttpServer.HTTP_UI_AUTHENTICATION, "").toLowerCase(); - String adminUsers = null; - if ("kerberos".equals(httpAuthType)) { - adminUsers = conf.get(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_USERS_KEY, null); - } else if ("ldap".equals(httpAuthType)) { - adminUsers = conf.get(HttpServer.HTTP_LDAP_AUTHENTICATION_ADMIN_USERS_KEY, null); - } + final String adminUsers = getAdminUsers(conf, httpAuthType); final String adminGroups = conf.get(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_GROUPS_KEY, null); if (adminUsers == null && adminGroups == null) { @@ -119,6 +114,16 @@ static AccessControlList buildAdminAcl(Configuration conf) { return new AccessControlList(adminUsers, adminGroups); } + private static String getAdminUsers(Configuration conf, String httpAuthType) { + if ("kerberos".equals(httpAuthType)) { + return conf.get(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_USERS_KEY, null); + } else if ("ldap".equals(httpAuthType)) { + return conf.get(HttpServer.HTTP_LDAP_AUTHENTICATION_ADMIN_USERS_KEY, null); + } + // If the auth type is not kerberos or ldap, return null + return null; + } + /** * Explicitly invoke {@link #addPrivilegedServlet(String, String, Class)} or * {@link #addUnprivilegedServlet(String, String, Class)} instead of this method. This method will diff --git a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/LdapServerTestBase.java b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/LdapServerTestBase.java index 4b599ebf35ae..bbf35b8585f6 100644 --- a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/LdapServerTestBase.java +++ b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/LdapServerTestBase.java @@ -82,7 +82,7 @@ public static void stopServer() throws Exception { * @param conf the Configuration object to set LDAP properties on * @return the configured Configuration object */ - protected static Configuration setLdapConfigurations(Configuration conf) { + protected static void setLdapConfigurations(Configuration conf) { conf.setInt(HttpServer.HTTP_MAX_THREADS, TestHttpServer.MAX_THREADS); // Enable LDAP (pre-req) @@ -94,7 +94,6 @@ protected static Configuration setLdapConfigurations(Configuration conf) { LdapConstants.LDAP_SERVER_ADDR, ldapRule.getLdapServer().getPort())); conf.set("hadoop.http.authentication.ldap.enablestarttls", "false"); conf.set("hadoop.http.authentication.ldap.basedn", LdapConstants.LDAP_BASE_DN); - return conf; } /** @@ -102,7 +101,7 @@ protected static Configuration setLdapConfigurations(Configuration conf) { * @param credentials the credentials to encode * @return the Basic Authentication header */ - String getBasicAuthHeader(String credentials) { + private String getBasicAuthHeader(String credentials) { return AUTH_TYPE + new Base64(0).encodeToString(credentials.getBytes()); } diff --git a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapAdminACL.java b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapAdminACL.java index b85282b332db..459865509630 100644 --- a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapAdminACL.java +++ b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapAdminACL.java @@ -81,14 +81,13 @@ public static void setupServer() throws Exception { LOG.info("HTTP server started: " + baseUrl); } - private static Configuration setLdapConfigurationWithACLs(Configuration conf) { + private static void setLdapConfigurationWithACLs(Configuration conf) { setLdapConfigurations(conf); // Enable LDAP admin ACL conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, true); conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_INSTRUMENTATION_REQUIRES_ADMIN, true); conf.set(HttpServer.HTTP_LDAP_AUTHENTICATION_ADMIN_USERS_KEY, "bjones"); - return conf; } @Test diff --git a/src/main/asciidoc/_chapters/security.adoc b/src/main/asciidoc/_chapters/security.adoc index 63a82459f7e5..7e4170b44d6d 100644 --- a/src/main/asciidoc/_chapters/security.adoc +++ b/src/main/asciidoc/_chapters/security.adoc @@ -275,6 +275,7 @@ The usernames listed in the above property should correspond to the LDAP usernam ==== Notes +* This feature is supported by only versions of HBase having https://issues.apache.org/jira/browse/HBASE-29244[HBASE-29244] * Ensure that the LDAP server is properly configured and running. See the previous section for details. * Only users explicitly listed in the `hbase.security.authentication.ldap.admin.users` property will have access to sensitive endpoints. * Non-administrative users can still access non-sensitive endpoints, provided they are authenticated.