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

jinsongzhou pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/amoro.git


The following commit(s) were added to refs/heads/master by this push:
     new 11989906a [Feature]: Support LDAP Authentication for Dashboard Login 
(#4009)
11989906a is described below

commit 11989906a57a2957cb7479f987c5d3b494f5fa07
Author: davedwwang <[email protected]>
AuthorDate: Thu Jan 8 19:59:28 2026 +0800

    [Feature]: Support LDAP Authentication for Dashboard Login (#4009)
    
    Co-authored-by: davedwwang <[email protected]>
---
 amoro-ams/pom.xml                                  | 29 +++++++
 .../apache/amoro/server/AmoroManagementConf.java   | 23 ++++++
 .../LdapPasswdAuthenticationProvider.java          | 80 ++++++++++++++++++
 .../dashboard/controller/LoginController.java      | 29 +++++--
 .../amoro/server/utils/PreconditionUtils.java      |  9 +++
 .../LdapPasswdAuthenticationProviderTest.java      | 94 ++++++++++++++++++++++
 amoro-ams/src/test/resources/ldap/users.ldif       | 37 +++++++++
 docs/configuration/ams-config.md                   |  3 +
 pom.xml                                            | 10 ++-
 9 files changed, 305 insertions(+), 9 deletions(-)

diff --git a/amoro-ams/pom.xml b/amoro-ams/pom.xml
index 98652eeb5..1724b8a80 100644
--- a/amoro-ams/pom.xml
+++ b/amoro-ams/pom.xml
@@ -486,6 +486,12 @@
             <groupId>org.apache.hadoop</groupId>
             <artifactId>hadoop-minikdc</artifactId>
             <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.directory.api</groupId>
+                    <artifactId>api-all</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
 
         <dependency>
@@ -494,12 +500,35 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.directory.api</groupId>
+            <artifactId>api-all</artifactId>
+            <scope>test</scope>
+        </dependency>
+
         <dependency>
             <groupId>com.google.guava</groupId>
             <artifactId>guava</artifactId>
             <version>${guava.version}</version>
             <scope>test</scope>
         </dependency>
+
+        <dependency>
+            <groupId>org.apache.directory.server</groupId>
+            <artifactId>apacheds-test-framework</artifactId>
+            <version>${apache-directory-server.version}</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.directory.api</groupId>
+                    <artifactId>api-ldap-schema-data</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.directory.jdbm</groupId>
+                    <artifactId>apacheds-jdbm1</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
     </dependencies>
 
     <build>
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java 
b/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java
index 579142b2e..2d59122d0 100644
--- a/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java
+++ b/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java
@@ -328,6 +328,29 @@ public class AmoroManagementConf {
               "User-defined password authentication implementation of"
                   + " 
org.apache.amoro.authentication.PasswdAuthenticationProvider");
 
+  public static final ConfigOption<String> HTTP_SERVER_LOGIN_AUTH_PROVIDER =
+      ConfigOptions.key("http-server.login-auth-provider")
+          .stringType()
+          .defaultValue(DefaultPasswdAuthenticationProvider.class.getName())
+          .withDescription(
+              "User-defined login authentication implementation of"
+                  + " 
org.apache.amoro.authentication.PasswdAuthenticationProvider");
+
+  public static final ConfigOption<String> 
HTTP_SERVER_LOGIN_AUTH_LDAP_USER_PATTERN =
+      ConfigOptions.key("http-server.login-auth-ldap-user-pattern")
+          .stringType()
+          .noDefaultValue()
+          .withDescription(
+              "LDAP user pattern for authentication. The pattern defines how 
to construct the user's distinguished name (DN) in the LDAP directory. "
+                  + "Use {0} as a placeholder for the username. For example, 
'cn={0},ou=people,dc=example,dc=com' will search for users in the specified 
organizational unit.");
+
+  public static final ConfigOption<String> HTTP_SERVER_LOGIN_AUTH_LDAP_URL =
+      ConfigOptions.key("http-server.login-auth-ldap-url")
+          .stringType()
+          .noDefaultValue()
+          .withDescription(
+              "LDAP connection URL(s), value could be a SPACE separated list 
of URLs to multiple LDAP servers for resiliency. URLs are tried in the order 
specified until the connection is successful");
+
   public static final ConfigOption<String> HTTP_SERVER_AUTH_JWT_PROVIDER =
       ConfigOptions.key("http-server.auth-jwt-provider")
           .stringType()
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/authentication/LdapPasswdAuthenticationProvider.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/authentication/LdapPasswdAuthenticationProvider.java
new file mode 100644
index 000000000..4ce70d793
--- /dev/null
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/authentication/LdapPasswdAuthenticationProvider.java
@@ -0,0 +1,80 @@
+/*
+ * 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.amoro.server.authentication;
+
+import org.apache.amoro.authentication.BasicPrincipal;
+import org.apache.amoro.authentication.PasswdAuthenticationProvider;
+import org.apache.amoro.authentication.PasswordCredential;
+import org.apache.amoro.config.Configurations;
+import org.apache.amoro.exception.SignatureCheckException;
+import org.apache.amoro.server.AmoroManagementConf;
+import org.apache.amoro.server.utils.PreconditionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.directory.InitialDirContext;
+
+import java.text.MessageFormat;
+import java.util.Hashtable;
+
+public class LdapPasswdAuthenticationProvider implements 
PasswdAuthenticationProvider {
+  public static final Logger LOG = 
LoggerFactory.getLogger(LdapPasswdAuthenticationProvider.class);
+
+  private String ldapUrl;
+  private String ldapUserParttern;
+  private MessageFormat formatter;
+
+  public LdapPasswdAuthenticationProvider(Configurations conf) {
+    this.ldapUrl = 
conf.get(AmoroManagementConf.HTTP_SERVER_LOGIN_AUTH_LDAP_URL);
+    PreconditionUtils.checkNotNullOrEmpty(
+        ldapUrl, AmoroManagementConf.HTTP_SERVER_LOGIN_AUTH_LDAP_URL.key());
+    this.ldapUserParttern = 
conf.get(AmoroManagementConf.HTTP_SERVER_LOGIN_AUTH_LDAP_USER_PATTERN);
+    PreconditionUtils.checkNotNullOrEmpty(
+        ldapUserParttern, 
AmoroManagementConf.HTTP_SERVER_LOGIN_AUTH_LDAP_USER_PATTERN.key());
+    this.formatter = new MessageFormat(ldapUserParttern);
+  }
+
+  @Override
+  public BasicPrincipal authenticate(PasswordCredential credential) throws 
SignatureCheckException {
+    Hashtable<String, Object> env = new Hashtable<String, Object>();
+    env.put(Context.INITIAL_CONTEXT_FACTORY, 
"com.sun.jndi.ldap.LdapCtxFactory");
+    env.put(Context.PROVIDER_URL, ldapUrl);
+    env.put(Context.SECURITY_AUTHENTICATION, "simple");
+    env.put(Context.SECURITY_CREDENTIALS, credential.password());
+    env.put(Context.SECURITY_PRINCIPAL, formatter.format(new String[] 
{credential.username()}));
+
+    InitialDirContext initialLdapContext = null;
+    try {
+      initialLdapContext = new InitialDirContext(env);
+    } catch (NamingException e) {
+      throw new SignatureCheckException("Failed to authenticate via ldap 
authentication", e);
+    } finally {
+      if (initialLdapContext != null) {
+        try {
+          initialLdapContext.close();
+        } catch (NamingException e) {
+          LOG.error("Exception in closing {}", initialLdapContext, e);
+        }
+      }
+    }
+    return new BasicPrincipal(credential.username());
+  }
+}
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/LoginController.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/LoginController.java
index 515d3b2c8..4d39705c0 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/LoginController.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/LoginController.java
@@ -19,22 +19,29 @@
 package org.apache.amoro.server.dashboard.controller;
 
 import io.javalin.http.Context;
+import org.apache.amoro.authentication.PasswdAuthenticationProvider;
 import org.apache.amoro.config.Configurations;
 import org.apache.amoro.server.AmoroManagementConf;
+import org.apache.amoro.server.authentication.DefaultPasswordCredential;
+import org.apache.amoro.server.authentication.HttpAuthenticationFactory;
 import org.apache.amoro.server.dashboard.response.OkResponse;
+import org.apache.amoro.server.utils.PreconditionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.Serializable;
 import java.util.Map;
 
 /** The controller that handles login requests. */
 public class LoginController {
+  public static final Logger LOG = 
LoggerFactory.getLogger(LoginController.class);
 
-  private final String adminUser;
-  private final String adminPassword;
+  private final PasswdAuthenticationProvider loginAuthProvider;
 
   public LoginController(Configurations serviceConfig) {
-    adminUser = serviceConfig.get(AmoroManagementConf.ADMIN_USERNAME);
-    adminPassword = serviceConfig.get(AmoroManagementConf.ADMIN_PASSWORD);
+    this.loginAuthProvider =
+        HttpAuthenticationFactory.getPasswordAuthenticationProvider(
+            
serviceConfig.get(AmoroManagementConf.HTTP_SERVER_LOGIN_AUTH_PROVIDER), 
serviceConfig);
   }
 
   /** Get current user. */
@@ -49,11 +56,17 @@ public class LoginController {
     Map<String, String> bodyParams = ctx.bodyAsClass(Map.class);
     String user = bodyParams.get("user");
     String pwd = bodyParams.get("password");
-    if (adminUser.equals(user) && (adminPassword.equals(pwd))) {
-      ctx.sessionAttribute("user", new SessionInfo(adminUser, 
System.currentTimeMillis() + ""));
+    PreconditionUtils.checkNotNullOrEmpty(user, "user");
+    PreconditionUtils.checkNotNullOrEmpty(pwd, "password");
+    DefaultPasswordCredential credential = new DefaultPasswordCredential(user, 
pwd);
+    try {
+      this.loginAuthProvider.authenticate(credential);
+      ctx.sessionAttribute("user", new SessionInfo(user, 
System.currentTimeMillis() + ""));
       ctx.json(OkResponse.of("success"));
-    } else {
-      throw new RuntimeException("invalid user " + user + " or password!");
+    } catch (Exception e) {
+      LOG.error("authenticate user {} failed", user, e);
+      String causeMessage = e.getMessage() != null ? e.getMessage() : "unknown 
error";
+      throw new RuntimeException("invalid user " + user + " or password! 
Cause: " + causeMessage);
     }
   }
 
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/utils/PreconditionUtils.java 
b/amoro-ams/src/main/java/org/apache/amoro/server/utils/PreconditionUtils.java
index 7db3b41ba..1a6f23508 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/utils/PreconditionUtils.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/utils/PreconditionUtils.java
@@ -34,4 +34,13 @@ public class PreconditionUtils {
       throw new AlreadyExistsException(objectName);
     }
   }
+
+  public static void checkNotNullOrEmpty(CharSequence arg, String argName) {
+    if (arg == null) {
+      throw new IllegalArgumentException(argName + " cannot be null");
+    }
+    if (arg.length() == 0) {
+      throw new IllegalArgumentException(argName + " cannot be empty");
+    }
+  }
 }
diff --git 
a/amoro-ams/src/test/java/org/apache/amoro/server/authentication/LdapPasswdAuthenticationProviderTest.java
 
b/amoro-ams/src/test/java/org/apache/amoro/server/authentication/LdapPasswdAuthenticationProviderTest.java
new file mode 100644
index 000000000..cf12ed052
--- /dev/null
+++ 
b/amoro-ams/src/test/java/org/apache/amoro/server/authentication/LdapPasswdAuthenticationProviderTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.amoro.server.authentication;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.apache.amoro.authentication.PasswdAuthenticationProvider;
+import org.apache.amoro.authentication.PasswordCredential;
+import org.apache.amoro.config.Configurations;
+import org.apache.amoro.exception.SignatureCheckException;
+import org.apache.amoro.server.AmoroManagementConf;
+import org.apache.directory.server.annotations.CreateLdapServer;
+import org.apache.directory.server.annotations.CreateTransport;
+import org.apache.directory.server.core.annotations.ApplyLdifFiles;
+import org.apache.directory.server.core.annotations.CreateDS;
+import org.apache.directory.server.core.annotations.CreatePartition;
+import org.apache.directory.server.core.integ.AbstractLdapTestUnit;
+import org.apache.directory.server.core.integ.FrameworkRunner;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.Principal;
+
+/**
+ * TestSuite to test amoro's LDAP Authentication provider with an in-process 
LDAP Server (Apache
+ * Directory Server instance).
+ *
+ * <p>refer to 
https://directory.apache.org/apacheds/advanced-ug/7-embedding-apacheds.html
+ */
+@RunWith(FrameworkRunner.class)
+@CreateLdapServer(
+    transports = {@CreateTransport(protocol = "LDAP"), 
@CreateTransport(protocol = "LDAPS")})
+@CreateDS(partitions = {@CreatePartition(name = "test", suffix = 
"dc=test,dc=com")})
+@ApplyLdifFiles({
+  "ldap/users.ldif",
+})
+public class LdapPasswdAuthenticationProviderTest extends AbstractLdapTestUnit 
{
+
+  @Test
+  public void testAuthenticate() throws Exception {
+    assertTrue(ldapServer.isStarted());
+    assertTrue(ldapServer.getPort() > 0);
+
+    Configurations conf = new Configurations();
+    String ldapUrl = "ldap://localhost:"; + ldapServer.getPort();
+    conf.set(
+        AmoroManagementConf.HTTP_SERVER_LOGIN_AUTH_PROVIDER,
+        LdapPasswdAuthenticationProvider.class.getName());
+    conf.set(AmoroManagementConf.HTTP_SERVER_LOGIN_AUTH_LDAP_URL, ldapUrl + " 
" + ldapUrl);
+    conf.set(
+        AmoroManagementConf.HTTP_SERVER_LOGIN_AUTH_LDAP_USER_PATTERN,
+        "cn={0},ou=Users,dc=test,dc=com");
+
+    PasswdAuthenticationProvider provider =
+        HttpAuthenticationFactory.getPasswordAuthenticationProvider(
+            conf.get(AmoroManagementConf.HTTP_SERVER_LOGIN_AUTH_PROVIDER), 
conf);
+
+    // Test successful authentication with correct password
+    PasswordCredential correctCredential = new 
DefaultPasswordCredential("user1", "12345");
+    Principal principal = provider.authenticate(correctCredential);
+    assertTrue(principal.getName().equals("user1"));
+
+    // Test successful authentication with incorrect password
+    assertThrows(
+        SignatureCheckException.class,
+        () -> {
+          provider.authenticate(new DefaultPasswordCredential("user1", 
"123456"));
+        });
+
+    // Test successful authentication with incorrect user
+    assertThrows(
+        SignatureCheckException.class,
+        () -> {
+          provider.authenticate(new DefaultPasswordCredential("user2", 
"12345"));
+        });
+  }
+}
diff --git a/amoro-ams/src/test/resources/ldap/users.ldif 
b/amoro-ams/src/test/resources/ldap/users.ldif
new file mode 100644
index 000000000..74efede6a
--- /dev/null
+++ b/amoro-ams/src/test/resources/ldap/users.ldif
@@ -0,0 +1,37 @@
+################################################################################
+#  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.
+################################################################################
+version: 1
+dn: dc=test,dc=com
+objectClass: domain
+objectClass: top
+dc: test
+
+dn: ou=Users,dc=test,dc=com
+objectClass: organizationalUnit
+objectClass: top
+ou: Users
+
+dn: cn=user1,ou=Users,dc=test,dc=com
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+cn: user1
+sn: Ldap
+uid: ldaptest1
+userPassword: 12345
diff --git a/docs/configuration/ams-config.md b/docs/configuration/ams-config.md
index 8e2e780d6..7a86a659c 100644
--- a/docs/configuration/ams-config.md
+++ b/docs/configuration/ams-config.md
@@ -82,6 +82,9 @@ table td:last-child, table th:last-child { width: 40%; 
word-break: break-all; }
 | http-server.auth-basic-provider | 
org.apache.amoro.server.authentication.DefaultPasswdAuthenticationProvider | 
User-defined password authentication implementation of 
org.apache.amoro.authentication.PasswdAuthenticationProvider |
 | http-server.auth-jwt-provider | &lt;undefined&gt; | User-defined JWT (JSON 
Web Token) authentication implementation of 
org.apache.amoro.authentication.TokenAuthenticationProvider |
 | http-server.bind-port | 19090 | Port that the Http server is bound to. |
+| http-server.login-auth-ldap-url | &lt;undefined&gt; | LDAP connection 
URL(s), value could be a SPACE separated list of URLs to multiple LDAP servers 
for resiliency. URLs are tried in the order specified until the connection is 
successful |
+| http-server.login-auth-ldap-user-pattern | &lt;undefined&gt; | LDAP user 
pattern for authentication. The pattern defines how to construct the user's 
distinguished name (DN) in the LDAP directory. Use {0} as a placeholder for the 
username. For example, 'cn={0},ou=people,dc=example,dc=com' will search for 
users in the specified organizational unit. |
+| http-server.login-auth-provider | 
org.apache.amoro.server.authentication.DefaultPasswdAuthenticationProvider | 
User-defined login authentication implementation of 
org.apache.amoro.authentication.PasswdAuthenticationProvider |
 | http-server.proxy-client-ip-header | X-Real-IP | The HTTP header to record 
the real client IP address. If your server is behind a load balancer or other 
proxy, the server will see this load balancer or proxy IP address as the client 
IP address, to get around this common issue, most load balancers or proxies 
offer the ability to record the real remote IP address in an HTTP header that 
will be added to the request for other devices to use. |
 | http-server.rest-auth-type | token | The authentication used by REST APIs, 
token (default), basic or jwt. |
 | http-server.session-timeout | 7 d | Timeout for http session. |
diff --git a/pom.xml b/pom.xml
index d21453cde..641407b88 100644
--- a/pom.xml
+++ b/pom.xml
@@ -163,7 +163,8 @@
         <pagehelper.version>6.1.0</pagehelper.version>
         <jsqlparser.version>4.7</jsqlparser.version>
         <fasterxml.jackson.version>2.14.2</fasterxml.jackson.version>
-
+        
<apache-directory-server.version>2.0.0-M15</apache-directory-server.version>
+        
<apache-directory-api-all.version>1.0.0-M20</apache-directory-api-all.version>
         <rocksdb-dependency-scope>compile</rocksdb-dependency-scope>
         <lucene-dependency-scope>compile</lucene-dependency-scope>
         <aliyun-sdk-dependency-scope>provided</aliyun-sdk-dependency-scope>
@@ -601,6 +602,13 @@
                 <scope>test</scope>
             </dependency>
 
+            <dependency>
+                <groupId>org.apache.directory.api</groupId>
+                <artifactId>api-all</artifactId>
+                <version>${apache-directory-api-all.version}</version>
+                <scope>test</scope>
+            </dependency>
+
             <dependency>
                 <groupId>org.slf4j</groupId>
                 <artifactId>slf4j-api</artifactId>

Reply via email to