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

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


The following commit(s) were added to refs/heads/master by this push:
     new 7b935fe4a7e HDDS-15168. Support configurable SASL mechanism (#10212)
7b935fe4a7e is described below

commit 7b935fe4a7edfab0bc4a82d30ed54a9e93536277
Author: Doroszlai, Attila <[email protected]>
AuthorDate: Sun May 10 21:09:13 2026 +0200

    HDDS-15168. Support configurable SASL mechanism (#10212)
---
 .../main/java/org/apache/hadoop/ipc_/Server.java   |   9 +-
 .../security_/CustomizedCallbackHandler.java       | 121 +++++++++++++++++++++
 .../hadoop/security_/SaslMechanismFactory.java     |  90 +++++++++++++++
 .../org/apache/hadoop/security_/SaslRpcClient.java |  16 ++-
 .../org/apache/hadoop/security_/SaslRpcServer.java |  63 ++++++-----
 5 files changed, 268 insertions(+), 31 deletions(-)

diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/Server.java 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/Server.java
index 2f767ae4bdf..bc0c829ed92 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/Server.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/Server.java
@@ -97,7 +97,9 @@
 import org.apache.hadoop.ipc_.protobuf.RpcHeaderProtos.RpcSaslProto.SaslState;
 import org.apache.hadoop.net.NetUtils;
 import org.apache.hadoop.security.AccessControlException;
+import org.apache.hadoop.security_.SaslMechanismFactory;
 import org.apache.hadoop.security.SaslPropertiesResolver;
+import org.apache.hadoop.security_.SaslRpcClient;
 import org.apache.hadoop.security_.SaslRpcServer;
 import org.apache.hadoop.security.SaslRpcServer.AuthMethod;
 import org.apache.hadoop.security.SecurityUtil;
@@ -1916,6 +1918,10 @@ public Server getServer() {
       return Server.this;
     }
 
+    public Configuration getConf() {
+      return Server.this.getConf();
+    }
+
     /* Return true if the connection has no outstanding rpc */
     private boolean isIdle() {
       return rpcCount.get() == 0;
@@ -2383,7 +2389,8 @@ private RpcSaslProto buildSaslNegotiateResponse()
       RpcSaslProto negotiateMessage = negotiateResponse;
       // accelerate token negotiation by sending initial challenge
       // in the negotiation response
-      if (enabledAuthMethods.contains(AuthMethod.TOKEN)) {
+      if (enabledAuthMethods.contains(AuthMethod.TOKEN)
+          && SaslMechanismFactory.isDigestMechanism(AuthMethod.TOKEN)) {
         saslServer = createSaslServer(AuthMethod.TOKEN);
         byte[] challenge = saslServer.evaluateResponse(new byte[0]);
         RpcSaslProto.Builder negotiateBuilder =
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/CustomizedCallbackHandler.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/CustomizedCallbackHandler.java
new file mode 100644
index 00000000000..2b60a4a971e
--- /dev/null
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/CustomizedCallbackHandler.java
@@ -0,0 +1,121 @@
+/*
+ * 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.security_;
+
+import org.apache.hadoop.conf.Configuration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** For handling customized {@link Callback}. */
+public interface CustomizedCallbackHandler {
+  Logger LOG = LoggerFactory.getLogger(CustomizedCallbackHandler.class);
+
+  class Cache {
+    private static final Map<String, CustomizedCallbackHandler> MAP = new 
HashMap<>();
+
+    private static synchronized CustomizedCallbackHandler getSynchronously(
+        String key, Configuration conf) {
+      //check again synchronously
+      final CustomizedCallbackHandler cached = MAP.get(key);
+      if (cached != null) {
+        return cached; //cache hit
+      }
+
+      //cache miss
+      final Class<?> clazz = conf.getClass(key, DefaultHandler.class);
+      LOG.debug("{} = {}", key, clazz);
+      if (clazz == DefaultHandler.class) {
+        return DefaultHandler.INSTANCE;
+      }
+
+      final Object created;
+      try {
+        created = clazz.newInstance();
+      } catch (Exception e) {
+        LOG.warn("Failed to create a new instance of {}, fallback to {}",
+            clazz, DefaultHandler.class, e);
+        return DefaultHandler.INSTANCE;
+      }
+
+      final CustomizedCallbackHandler handler = created instanceof 
CustomizedCallbackHandler ?
+          (CustomizedCallbackHandler) created : 
CustomizedCallbackHandler.delegate(created);
+      MAP.put(key, handler);
+      return handler;
+    }
+
+    private static CustomizedCallbackHandler get(String key, Configuration 
conf) {
+      final CustomizedCallbackHandler cached = MAP.get(key);
+      return cached != null ? cached : getSynchronously(key, conf);
+    }
+
+    public static synchronized void clear() {
+      MAP.clear();
+    }
+
+    private Cache() { }
+  }
+
+  class DefaultHandler implements CustomizedCallbackHandler {
+    private static final DefaultHandler INSTANCE = new DefaultHandler();
+
+    @Override
+    public void handleCallbacks(List<Callback> callbacks, String username, 
char[] password)
+        throws UnsupportedCallbackException {
+      if (!callbacks.isEmpty()) {
+        final Callback cb = callbacks.get(0);
+        throw new UnsupportedCallbackException(callbacks.get(0),
+            "Unsupported callback: " + (cb == null ? null : cb.getClass()));
+      }
+    }
+  }
+
+  static CustomizedCallbackHandler delegate(Object delegated) {
+    final String methodName = "handleCallbacks";
+    final Class<?> clazz = delegated.getClass();
+    final Method method;
+    try {
+      method = clazz.getMethod(methodName, List.class, String.class, 
char[].class);
+    } catch (NoSuchMethodException e) {
+      throw new IllegalStateException("Failed to get method " + methodName + " 
from " + clazz, e);
+    }
+
+    return (callbacks, name, password) -> {
+      try {
+        method.invoke(delegated, callbacks, name, password);
+      } catch (IllegalAccessException | InvocationTargetException e) {
+        throw new IOException("Failed to invoke " + method, e);
+      }
+    };
+  }
+
+  static CustomizedCallbackHandler get(String key, Configuration conf) {
+    return Cache.get(key, conf);
+  }
+
+  void handleCallbacks(List<Callback> callbacks, String name, char[] password)
+      throws UnsupportedCallbackException, IOException;
+}
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslMechanismFactory.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslMechanismFactory.java
new file mode 100644
index 00000000000..f3b9f92934e
--- /dev/null
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslMechanismFactory.java
@@ -0,0 +1,90 @@
+/*
+ * 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.security_;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.security.SaslRpcServer.AuthMethod;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * SASL related constants.
+ */
+public final class SaslMechanismFactory {
+  static final Logger LOG = 
LoggerFactory.getLogger(SaslMechanismFactory.class);
+
+  public static final String HADOOP_SECURITY_SASL_MECHANISM_KEY
+      = "hadoop.security.sasl.mechanism";
+  public static final String HADOOP_SECURITY_SASL_MECHANISM_DEFAULT
+      = "DIGEST-MD5";
+  public static final String 
HADOOP_SECURITY_SASL_CUSTOMIZEDCALLBACKHANDLER_CLASS_KEY
+      = "hadoop.security.sasl.CustomizedCallbackHandler.class";
+
+  private static final String SASL_MECHANISM_ENV = "HADOOP_SASL_MECHANISM";
+  private static volatile String mechanism;
+
+  private static synchronized String getSynchronously() {
+    // env
+    final String envValue = System.getenv(SASL_MECHANISM_ENV);
+    LOG.debug("{} = {} (env)", SASL_MECHANISM_ENV, envValue);
+
+    // conf
+    final Configuration conf = new Configuration();
+    final String confValue = conf.get(HADOOP_SECURITY_SASL_MECHANISM_KEY,
+        HADOOP_SECURITY_SASL_MECHANISM_DEFAULT);
+    LOG.debug("{} = {} (conf)", HADOOP_SECURITY_SASL_MECHANISM_KEY, confValue);
+
+    mechanism = envValue != null ? envValue
+        : confValue != null ? confValue
+        : HADOOP_SECURITY_SASL_MECHANISM_DEFAULT;
+    LOG.debug("SASL_MECHANISM = {} (effective)", mechanism);
+    return mechanism;
+  }
+
+  public static String getMechanism() {
+    final String value = mechanism;
+    return value != null ? value : getSynchronously();
+  }
+
+  public static boolean isDefaultMechanism(AuthMethod authMethod) {
+    return 
HADOOP_SECURITY_SASL_MECHANISM_DEFAULT.equals(getMechanismName(authMethod));
+  }
+
+  public static boolean isDigestMechanism(AuthMethod authMethod) {
+    return getMechanismName(authMethod).startsWith("DIGEST-");
+  }
+
+  private SaslMechanismFactory() {}
+
+  public static void main(String[] args) {
+    System.out.println("SASL_MECHANISM = " + getMechanism());
+  }
+
+  /** Helper to get actual mechanism name from config.  Required because 
{@code AuthMethod} is from Hadoop,
+   * not forked (because it is used in UGI, etc.). */
+  public static String getMechanismName(AuthMethod authMethod) {
+    switch (authMethod) {
+    case DIGEST:
+    case TOKEN:
+      return getMechanism();
+    default:
+      return authMethod.getMechanismName();
+
+    }
+  }
+}
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslRpcClient.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslRpcClient.java
index 8efeb073810..d3c0cedebad 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslRpcClient.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslRpcClient.java
@@ -39,6 +39,7 @@
 import javax.security.auth.callback.PasswordCallback;
 import javax.security.auth.callback.UnsupportedCallbackException;
 import javax.security.auth.kerberos.KerberosPrincipal;
+import javax.security.sasl.AuthorizeCallback;
 import javax.security.sasl.RealmCallback;
 import javax.security.sasl.RealmChoiceCallback;
 import javax.security.sasl.Sasl;
@@ -185,7 +186,7 @@ private boolean isValidAuthType(SaslAuth authType) {
     }
     // do we know what it is?  is it using our mechanism?
     return authMethod != null &&
-           authMethod.getMechanismName().equals(authType.getMechanism());
+           
SaslMechanismFactory.getMechanismName(authMethod).equals(authType.getMechanism());
   }
 
   /**
@@ -242,7 +243,7 @@ private SaslClient createSaslClient(SaslAuth authType)
         throw new IOException("Unknown authentication method " + method);
     }
 
-    String mechanism = method.getMechanismName();
+    String mechanism = SaslMechanismFactory.getMechanismName(method);
     if (LOG.isDebugEnabled()) {
       LOG.debug("Creating SASL " + mechanism + "(" + method + ") "
           + " client to authenticate to service at " + saslServerName);
@@ -664,9 +665,17 @@ public void handle(Callback[] callbacks)
           pc = (PasswordCallback) callback;
         } else if (callback instanceof RealmCallback) {
           rc = (RealmCallback) callback;
+        } else if (callback instanceof AuthorizeCallback) {
+          final AuthorizeCallback ac = (AuthorizeCallback) callback;
+          final String authId = ac.getAuthenticationID();
+          final String authzId = ac.getAuthorizationID();
+          ac.setAuthorized(authId.equals(authzId));
+          if (ac.isAuthorized()) {
+            ac.setAuthorizedID(authzId);
+          }
         } else {
           throw new UnsupportedCallbackException(callback,
-              "Unrecognized SASL client callback");
+              "Unrecognized SASL client callback " + callback.getClass());
         }
       }
       if (nc != null) {
@@ -712,4 +721,5 @@ public static String getHostName(String name) {
       }
     }
   }
+
 }
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslRpcServer.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslRpcServer.java
index 0fef4f21f8d..8f82854ed17 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslRpcServer.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslRpcServer.java
@@ -18,10 +18,10 @@
 
 package org.apache.hadoop.security_;
 
+import static 
org.apache.hadoop.security_.SaslMechanismFactory.HADOOP_SECURITY_SASL_CUSTOMIZEDCALLBACKHANDLER_CLASS_KEY;
+
 import java.io.ByteArrayInputStream;
-import java.io.DataInput;
 import java.io.DataInputStream;
-import java.io.DataOutput;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.security.PrivilegedExceptionAction;
@@ -46,10 +46,8 @@
 
 import org.apache.commons.codec.binary.Base64;
 import org.apache.hadoop.conf.Configuration;
-import org.apache.hadoop.ipc_.RetriableException;
 import org.apache.hadoop.ipc_.Server;
 import org.apache.hadoop.ipc_.Server.Connection;
-import org.apache.hadoop.ipc_.StandbyException;
 import org.apache.hadoop.security.AccessControlException;
 import org.apache.hadoop.security.SaslPlainServer;
 import org.apache.hadoop.security.SaslRpcServer.AuthMethod;
@@ -91,7 +89,7 @@ public String getSaslQop() {
   
   public SaslRpcServer(AuthMethod authMethod) throws IOException {
     this.authMethod = authMethod;
-    mechanism = authMethod.getMechanismName();    
+    mechanism = SaslMechanismFactory.getMechanismName(authMethod);
     switch (authMethod) {
       case SIMPLE: {
         return; // no sasl for simple
@@ -214,30 +212,46 @@ public static String[] splitKerberosName(String fullName) 
{
     return fullName.split("[/@]");
   }
 
-  /** CallbackHandler for SASL DIGEST-MD5 mechanism */
+  /** CallbackHandler for SASL mechanism */
   public static class SaslDigestCallbackHandler implements CallbackHandler {
+    private final CustomizedCallbackHandler customizedCallbackHandler;
     private SecretManager<TokenIdentifier> secretManager;
     private Server.Connection connection; 
     
     public SaslDigestCallbackHandler(
         SecretManager<TokenIdentifier> secretManager,
         Server.Connection connection) {
+      this(secretManager, connection, connection.getConf());
+    }
+
+    public SaslDigestCallbackHandler(
+        SecretManager<TokenIdentifier> secretManager,
+        Server.Connection connection,
+        Configuration conf) {
       this.secretManager = secretManager;
       this.connection = connection;
+      this.customizedCallbackHandler = CustomizedCallbackHandler.get(
+          HADOOP_SECURITY_SASL_CUSTOMIZEDCALLBACKHANDLER_CLASS_KEY, conf);
     }
 
-    private char[] getPassword(TokenIdentifier tokenid) throws InvalidToken,
-        StandbyException, RetriableException, IOException {
+    private char[] getPassword(TokenIdentifier tokenid) throws IOException {
       return encodePassword(secretManager.retriableRetrievePassword(tokenid));
     }
 
+    private char[] getPassword(String name) throws IOException {
+      final TokenIdentifier tokenIdentifier = getIdentifier(name, 
secretManager);
+      final UserGroupInformation user = tokenIdentifier.getUser();
+      connection.attemptingUser = user;
+      LOG.debug("SASL server callback: setting password for client: {}", user);
+      return getPassword(tokenIdentifier);
+    }
+
     @Override
-    public void handle(Callback[] callbacks) throws InvalidToken,
-        UnsupportedCallbackException, StandbyException, RetriableException,
-        IOException {
+    public void handle(Callback[] callbacks) throws 
UnsupportedCallbackException, IOException {
       NameCallback nc = null;
       PasswordCallback pc = null;
       AuthorizeCallback ac = null;
+      List<Callback> unknownCallbacks = null;
       for (Callback callback : callbacks) {
         if (callback instanceof AuthorizeCallback) {
           ac = (AuthorizeCallback) callback;
@@ -248,23 +262,14 @@ public void handle(Callback[] callbacks) throws 
InvalidToken,
         } else if (callback instanceof RealmCallback) {
           continue; // realm is ignored
         } else {
-          throw new UnsupportedCallbackException(callback,
-              "Unrecognized SASL DIGEST-MD5 Callback");
+          if (unknownCallbacks == null) {
+            unknownCallbacks = new ArrayList<>();
+          }
+          unknownCallbacks.add(callback);
         }
       }
       if (pc != null) {
-        TokenIdentifier tokenIdentifier = getIdentifier(nc.getDefaultName(),
-            secretManager);
-        char[] password = getPassword(tokenIdentifier);
-        UserGroupInformation user = null;
-        user = tokenIdentifier.getUser(); // may throw exception
-        connection.attemptingUser = user;
-        
-        if (LOG.isDebugEnabled()) {
-          LOG.debug("SASL server DIGEST-MD5 callback: setting password "
-              + "for client: " + tokenIdentifier.getUser());
-        }
-        pc.setPassword(password);
+        pc.setPassword(getPassword(nc.getDefaultName()));
       }
       if (ac != null) {
         String authid = ac.getAuthenticationID();
@@ -279,12 +284,16 @@ public void handle(Callback[] callbacks) throws 
InvalidToken,
             UserGroupInformation logUser =
               getIdentifier(authzid, secretManager).getUser();
             String username = logUser == null ? null : logUser.getUserName();
-            LOG.debug("SASL server DIGEST-MD5 callback: setting "
-                + "canonicalized client ID: " + username);
+            LOG.debug("SASL server callback: setting authorizedID: {}", 
username);
           }
           ac.setAuthorizedID(authzid);
         }
       }
+      if (unknownCallbacks != null) {
+        final String name = nc != null ? nc.getDefaultName() : null;
+        final char[] password = name != null ? getPassword(name) : null;
+        customizedCallbackHandler.handleCallbacks(unknownCallbacks, name, 
password);
+      }
     }
   }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to