This is an automated email from the ASF dual-hosted git repository.

pdallig pushed a commit to branch branch-0.12
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/branch-0.12 by this push:
     new abb4983d1c [ZEPPELIN-6186] fix searching user using JdbcRealm
abb4983d1c is described below

commit abb4983d1c05c90b6a1c0dda910165d34d2d8194
Author: Soonoh <soonoh.j...@gmail.com>
AuthorDate: Fri Jun 6 19:37:35 2025 +0900

    [ZEPPELIN-6186] fix searching user using JdbcRealm
    
    ### What is this PR for?
    
    This PR fixes a broken feature where getUserList fails when using JdbcRealm 
for Shiro authentication.
    
    The previous PR addressing SQL injection introduced an error that prevents 
getUserList from working correctly with JdbcRealm. As a result, users are 
unable to set notebook permissions.
    
    Ref: #4676
    
    ### What type of PR is it?
    
    Bug Fix
    
    ### Todos
    
    ### What is the Jira issue?
    
    * https://issues.apache.org/jira/browse/ZEPPELIN-3725
    * https://issues.apache.org/jira/browse/ZEPPELIN-6186
    
    ### How should this be tested?
    
    Added new unit test should be passed
    
    * `./mvnw clean package -pl zeppelin-server -am`
    
    Verify in UI
    
    * Go to notebook permission settings
    * Type username and see if username listed
    
    ### Screenshots (if appropriate)
    
    ### Questions:
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    Closes #4926 from sh1nj1/fix/user-list-in-jdbc.
    
    Signed-off-by: Philipp Dallig <philipp.dal...@gmail.com>
    (cherry picked from commit 1221444ec64a43bd65e5e65654b8c0388b34aebf)
    Signed-off-by: Philipp Dallig <philipp.dal...@gmail.com>
---
 zeppelin-server/pom.xml                            |  8 ++++
 .../service/ShiroAuthenticationService.java        | 46 ++++++++++++++++------
 .../service/ShiroAuthenticationServiceTest.java    | 34 ++++++++++++++++
 3 files changed, 75 insertions(+), 13 deletions(-)

diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml
index 5bcfc7e8e3..f791a6c22c 100644
--- a/zeppelin-server/pom.xml
+++ b/zeppelin-server/pom.xml
@@ -45,6 +45,7 @@
     <selenium.java.version>2.48.2</selenium.java.version>
     <xml.apis.version>1.4.01</xml.apis.version>
     <hamcrest.version>2.2</hamcrest.version>
+    <h2.version>2.3.232</h2.version>
   </properties>
 
   <dependencies>
@@ -368,6 +369,13 @@
       <scope>test</scope>
     </dependency>
 
+    <dependency>
+      <groupId>com.h2database</groupId>
+      <artifactId>h2</artifactId>
+      <version>${h2.version}</version>
+      <scope>test</scope>
+    </dependency>
+
       <dependency>
         <groupId>org.bitbucket.cowwoc</groupId>
         <artifactId>diff-match-patch</artifactId>
diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/service/ShiroAuthenticationService.java
 
b/zeppelin-server/src/main/java/org/apache/zeppelin/service/ShiroAuthenticationService.java
index 72e6ffb9d8..14a2abe52c 100644
--- 
a/zeppelin-server/src/main/java/org/apache/zeppelin/service/ShiroAuthenticationService.java
+++ 
b/zeppelin-server/src/main/java/org/apache/zeppelin/service/ShiroAuthenticationService.java
@@ -26,7 +26,8 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import jakarta.inject.Inject;
+import java.util.regex.Pattern;
+
 import javax.naming.NamingEnumeration;
 import javax.naming.NamingException;
 import javax.naming.directory.Attributes;
@@ -34,6 +35,8 @@ import javax.naming.directory.SearchControls;
 import javax.naming.directory.SearchResult;
 import javax.naming.ldap.LdapContext;
 import javax.sql.DataSource;
+
+import jakarta.inject.Inject;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.reflect.FieldUtils;
 import org.apache.shiro.UnavailableSecurityManagerException;
@@ -68,6 +71,12 @@ public class ShiroAuthenticationService implements 
AuthenticationService {
   private static final String ACTIVE_DIRECTORY_GROUP_REALM = 
"org.apache.zeppelin.realm.ActiveDirectoryGroupRealm";
   private static final String JDBC_REALM = 
"org.apache.shiro.realm.jdbc.JdbcRealm";
 
+  private static final Pattern VALID_SQL_NAME_IDENTIFIER_PATTERN = 
Pattern.compile("^[a-zA-Z0-9_]+$");
+
+  private static boolean isValidSqlIdentifier(String name) {
+    return name != null && 
VALID_SQL_NAME_IDENTIFIER_PATTERN.matcher(name).matches();
+  }
+
   private final ZeppelinConfiguration zConf;
 
   @Inject
@@ -171,10 +180,9 @@ public class ShiroAuthenticationService implements 
AuthenticationService {
           } else if (LDAP_REALM.equals(realClassName)) {
             usersList.addAll(getUserList((LdapRealm) realm, searchText, 
numUsersToFetch));
           } else if (ACTIVE_DIRECTORY_GROUP_REALM.equals(realClassName)) {
-            usersList.addAll(
-                getUserList((ActiveDirectoryGroupRealm) realm, searchText, 
numUsersToFetch));
+            usersList.addAll(getUserList((ActiveDirectoryGroupRealm) realm, 
searchText, numUsersToFetch));
           } else if (JDBC_REALM.equals(realClassName)) {
-            usersList.addAll(getUserList((JdbcRealm) realm));
+            usersList.addAll(getUserList((JdbcRealm) realm, searchText, 
numUsersToFetch));
           }
         }
       }
@@ -401,7 +409,7 @@ public class ShiroAuthenticationService implements 
AuthenticationService {
   }
 
   /** Function to extract users from JDBCs. */
-  private List<String> getUserList(JdbcRealm obj) {
+  private List<String> getUserList(JdbcRealm obj, String searchText, int 
numUsersToFetch) {
     List<String> userlist = new ArrayList<>();
     Connection con = null;
     PreparedStatement ps = null;
@@ -415,26 +423,39 @@ public class ShiroAuthenticationService implements 
AuthenticationService {
     try {
       dataSource = (DataSource) FieldUtils.readField(obj, "dataSource", true);
       authQuery = (String) FieldUtils.readField(obj, "authenticationQuery", 
true);
-      LOGGER.info(authQuery);
+      LOGGER.debug("authenticationQuery={}", authQuery);
       String authQueryLowerCase = authQuery.toLowerCase();
       retval = authQueryLowerCase.split("from", 2);
       if (retval.length >= 2) {
         retval = retval[1].split("with|where", 2);
-        tablename = retval[0];
+        tablename = retval[0].strip();
         retval = retval[1].split("where", 2);
         if (retval.length >= 2) {
           retval = retval[1].split("=", 2);
         } else {
           retval = retval[0].split("=", 2);
         }
-        username = retval[0];
+        username = retval[0].strip();
       }
 
       if (StringUtils.isBlank(username) || StringUtils.isBlank(tablename)) {
         return userlist;
       }
+      if (!isValidSqlIdentifier(username)) {
+        throw new IllegalArgumentException(
+          "Invalid column name in authenticationQuery to build userlist query: 
"
+            + authQuery + ", allowed pattern: " + 
VALID_SQL_NAME_IDENTIFIER_PATTERN
+            + ", name identifier: [" + username + "]");
+      }
+      if (!isValidSqlIdentifier(tablename)) {
+        throw new IllegalArgumentException(
+          "Invalid table name in authenticationQuery to build userlist query: "
+            + authQuery + ", allowed pattern: " + 
VALID_SQL_NAME_IDENTIFIER_PATTERN
+            + ", name identifier: [" + tablename + "]");
+      }
 
-      userquery = "SELECT ? FROM ?";
+      userquery = String.format("SELECT %s FROM %s WHERE %s LIKE ?", username, 
tablename, username);
+      LOGGER.info("Built query for user list. userquery={}", userquery);
     } catch (IllegalAccessException e) {
       LOGGER.error("Error while accessing dataSource for JDBC Realm", e);
       return new ArrayList<>();
@@ -443,11 +464,10 @@ public class ShiroAuthenticationService implements 
AuthenticationService {
     try {
       con = dataSource.getConnection();
       ps = con.prepareStatement(userquery);
-      ps.setString(1, username);
-      ps.setString(2, tablename);
+      ps.setString(1, "%" + searchText + "%");
       rs = ps.executeQuery();
-      while (rs.next()) {
-        userlist.add(rs.getString(1).trim());
+      while (rs.next() && userlist.size() < numUsersToFetch) {
+        userlist.add(rs.getString(1));
       }
     } catch (Exception e) {
       LOGGER.error("Error retrieving User list from JDBC Realm", e);
diff --git 
a/zeppelin-server/src/test/java/org/apache/zeppelin/service/ShiroAuthenticationServiceTest.java
 
b/zeppelin-server/src/test/java/org/apache/zeppelin/service/ShiroAuthenticationServiceTest.java
index c0c7fe6c94..f82539e715 100644
--- 
a/zeppelin-server/src/test/java/org/apache/zeppelin/service/ShiroAuthenticationServiceTest.java
+++ 
b/zeppelin-server/src/test/java/org/apache/zeppelin/service/ShiroAuthenticationServiceTest.java
@@ -23,17 +23,22 @@ import static org.mockito.Mockito.spy;
 
 import java.io.IOException;
 import java.security.Principal;
+import java.sql.Connection;
+import java.sql.Statement;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.shiro.mgt.DefaultSecurityManager;
+import org.apache.shiro.realm.jdbc.JdbcRealm;
 import org.apache.shiro.subject.Subject;
 import org.apache.shiro.util.LifecycleUtils;
 import org.apache.shiro.util.ThreadContext;
 import org.apache.zeppelin.conf.ZeppelinConfiguration;
 import org.apache.zeppelin.realm.jwt.KnoxJwtRealm;
 import org.apache.zeppelin.service.shiro.AbstractShiroTest;
+import org.h2.jdbcx.JdbcDataSource;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -53,6 +58,35 @@ class ShiroAuthenticationServiceTest extends 
AbstractShiroTest {
     shiroSecurityService = new ShiroAuthenticationService(zConf);
   }
 
+  @Test
+  void testGetMatchedUsersWithJdbcRealm() throws Exception {
+
+    // given in-memory jdbcRealm with some users
+    JdbcRealm realm = new JdbcRealm();
+    JdbcDataSource dataSource = new JdbcDataSource();
+    dataSource.setURL("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1");
+    dataSource.setUser("sa");
+    realm.setDataSource(dataSource);
+
+    LifecycleUtils.init(realm);
+    DefaultSecurityManager securityManager = new DefaultSecurityManager(realm);
+    ThreadContext.bind(securityManager);
+
+    try (Connection conn = dataSource.getConnection(); Statement stmt = 
conn.createStatement()) {
+      stmt.execute("CREATE TABLE users (username VARCHAR PRIMARY KEY, password 
VARCHAR)");
+      stmt.execute("INSERT INTO users VALUES ('admin', '')");
+      stmt.execute("INSERT INTO users VALUES ('admin1', '')");
+      stmt.execute("INSERT INTO users VALUES ('test', '')");
+    }
+
+    // when
+    List<String> users = shiroSecurityService.getMatchedUsers("adm", 1);
+
+    // then
+    assertEquals(1, users.size());
+    assertEquals("admin", users.get(0));
+  }
+
   @Test
   void canGetPrincipalName() {
     String expectedName = "java.security.Principal.getName()";

Reply via email to