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 | <undefined> | 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 | <undefined> | 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 | <undefined> | 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>