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

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


The following commit(s) were added to refs/heads/master by this push:
     new c1c06f99010 optimize user login (#17242)
c1c06f99010 is described below

commit c1c06f99010da3f851112ce97377c50593e8b070
Author: Hongkun Xu <[email protected]>
AuthorDate: Wed Dec 24 13:39:46 2025 +0800

    optimize user login (#17242)
    
    Signed-off-by: Hongkun Xu <[email protected]>
---
 .../broker/ZkBasicAuthAccessControlFactory.java    | 43 +++++++++++++++-------
 .../api/access/AuthenticationFilter.java           |  2 +-
 .../api/access/BasicAuthAccessControlFactory.java  |  6 +++
 .../access/ZkBasicAuthAccessControlFactory.java    | 43 ++++++++++++++++------
 .../api/resources/PinotControllerAuthResource.java | 26 +++++++++++++
 .../src/main/resources/app/components/Layout.tsx   |  2 +-
 .../src/main/resources/app/requests/index.ts       |  2 +-
 .../pinot/core/auth/FineGrainedAccessControl.java  | 11 ++++++
 8 files changed, 107 insertions(+), 28 deletions(-)

diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/broker/ZkBasicAuthAccessControlFactory.java
 
b/pinot-broker/src/main/java/org/apache/pinot/broker/broker/ZkBasicAuthAccessControlFactory.java
index 6102b56bf3a..518bbe6c233 100644
--- 
a/pinot-broker/src/main/java/org/apache/pinot/broker/broker/ZkBasicAuthAccessControlFactory.java
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/broker/ZkBasicAuthAccessControlFactory.java
@@ -22,11 +22,11 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import javax.ws.rs.NotAuthorizedException;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.helix.store.zk.ZkHelixPropertyStore;
 import org.apache.helix.zookeeper.datamodel.ZNRecord;
 import org.apache.pinot.broker.api.AccessControl;
@@ -122,24 +122,39 @@ public class ZkBasicAuthAccessControlFactory extends 
AccessControlFactory {
 
     private Optional<ZkBasicAuthPrincipal> getPrincipalAuth(RequesterIdentity 
requesterIdentity) {
       Collection<String> tokens = 
extractAuthorizationTokens(requesterIdentity);
-      if (tokens.isEmpty()) {
+      if (tokens == null || tokens.isEmpty()) {
         return Optional.empty();
       }
 
-      _name2principal = 
BasicAuthUtils.extractBasicAuthPrincipals(_userCache.getAllBrokerUserConfig()).stream()
-          .collect(Collectors.toMap(BasicAuthPrincipal::getName, p -> p));
+      Map<String, ZkBasicAuthPrincipal> name2principal =
+          
BasicAuthUtils.extractBasicAuthPrincipals(_userCache.getAllBrokerUserConfig()).stream()
+              .collect(Collectors.toMap(BasicAuthPrincipal::getName, p -> p));
 
-      Map<String, String> name2password = tokens.stream().collect(
-          Collectors.toMap(
-              org.apache.pinot.common.auth.BasicAuthUtils::extractUsername,
-              org.apache.pinot.common.auth.BasicAuthUtils::extractPassword,
-              (v1, v2) -> v2));
-      Map<String, ZkBasicAuthPrincipal> password2principal =
-          
name2password.keySet().stream().collect(Collectors.toMap(name2password::get, 
_name2principal::get));
+      for (String token : tokens) {
+        String username = 
org.apache.pinot.common.auth.BasicAuthUtils.extractUsername(token);
+        String password = 
org.apache.pinot.common.auth.BasicAuthUtils.extractPassword(token);
 
-      return password2principal.entrySet().stream().filter(
-          entry -> BcryptUtils.checkpwWithCache(entry.getKey(), 
entry.getValue().getPassword(),
-              _userCache.getUserPasswordAuthCache())).map(u -> 
u.getValue()).filter(Objects::nonNull).findFirst();
+        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
+          continue;
+        }
+
+        ZkBasicAuthPrincipal principal = name2principal.get(username);
+        if (principal == null) {
+          continue;
+        }
+
+        if (passwordMatches(principal, password)) {
+          return Optional.of(principal);
+        }
+      }
+      return Optional.empty();
+    }
+
+    private boolean passwordMatches(ZkBasicAuthPrincipal principal, String 
password) {
+      return BcryptUtils.checkpwWithCache(
+          password,
+          principal.getPassword(),
+          _userCache.getUserPasswordAuthCache());
     }
   }
 }
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AuthenticationFilter.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AuthenticationFilter.java
index 6495115d00c..d99bce491c9 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AuthenticationFilter.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AuthenticationFilter.java
@@ -51,7 +51,7 @@ import org.glassfish.grizzly.http.server.Request;
 @javax.ws.rs.ext.Provider
 public class AuthenticationFilter implements ContainerRequestFilter {
   private static final Set<String> UNPROTECTED_PATHS =
-      new HashSet<>(Arrays.asList("", "help", "auth/info", "auth/verify", 
"health"));
+      new HashSet<>(Arrays.asList("", "help", "auth/info", "auth/verify", 
"auth/verify/v2", "health"));
   private static final String KEY_TABLE_NAME = "tableName";
   private static final String KEY_TABLE_NAME_WITH_TYPE = "tableNameWithType";
   private static final String KEY_SCHEMA_NAME = "schemaName";
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/BasicAuthAccessControlFactory.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/BasicAuthAccessControlFactory.java
index 8147ac36755..05cbf0fddc9 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/BasicAuthAccessControlFactory.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/BasicAuthAccessControlFactory.java
@@ -28,6 +28,7 @@ import javax.ws.rs.NotAuthorizedException;
 import javax.ws.rs.core.HttpHeaders;
 import org.apache.pinot.core.auth.BasicAuthPrincipal;
 import org.apache.pinot.core.auth.BasicAuthUtils;
+import org.apache.pinot.core.auth.TargetType;
 import org.apache.pinot.spi.env.PinotConfiguration;
 
 
@@ -90,6 +91,11 @@ public class BasicAuthAccessControlFactory implements 
AccessControlFactory {
       return true;
     }
 
+    @Override
+    public boolean hasAccess(HttpHeaders httpHeaders, TargetType targetType) {
+      return getPrincipal(httpHeaders).isPresent();
+    }
+
     private Optional<BasicAuthPrincipal> getPrincipal(HttpHeaders headers) {
       if (headers == null) {
         return Optional.empty();
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/ZkBasicAuthAccessControlFactory.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/ZkBasicAuthAccessControlFactory.java
index b78229912fb..75cec75f9c9 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/ZkBasicAuthAccessControlFactory.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/ZkBasicAuthAccessControlFactory.java
@@ -25,11 +25,13 @@ import java.util.Objects;
 import java.util.Optional;
 import java.util.stream.Collectors;
 import javax.ws.rs.core.HttpHeaders;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.pinot.common.config.provider.AccessControlUserCache;
 import org.apache.pinot.common.utils.BcryptUtils;
 import org.apache.pinot.controller.ControllerConf;
 import org.apache.pinot.controller.helix.core.PinotHelixResourceManager;
 import org.apache.pinot.core.auth.BasicAuthUtils;
+import org.apache.pinot.core.auth.TargetType;
 import org.apache.pinot.core.auth.ZkBasicAuthPrincipal;
 import org.apache.pinot.spi.env.PinotConfiguration;
 import org.apache.pinot.spi.utils.builder.TableNameBuilder;
@@ -85,6 +87,11 @@ public class ZkBasicAuthAccessControlFactory implements 
AccessControlFactory {
               && p.hasPermission(Objects.toString(accessType))).isPresent();
     }
 
+    @Override
+    public boolean hasAccess(HttpHeaders httpHeaders, TargetType targetType) {
+      return getPrincipal(httpHeaders).isPresent();
+    }
+
     @Override
     public boolean hasAccess(AccessType accessType, HttpHeaders httpHeaders, 
String endpointUrl) {
       return getPrincipal(httpHeaders).isPresent();
@@ -102,17 +109,31 @@ public class ZkBasicAuthAccessControlFactory implements 
AccessControlFactory {
       if (authHeaders == null) {
         return Optional.empty();
       }
-      Map<String, String> name2password = authHeaders.stream().collect(
-          Collectors.toMap(
-              org.apache.pinot.common.auth.BasicAuthUtils::extractUsername,
-              org.apache.pinot.common.auth.BasicAuthUtils::extractPassword,
-              (v1, v2) -> v2));
-      Map<String, ZkBasicAuthPrincipal> password2principal =
-          
name2password.keySet().stream().collect(Collectors.toMap(name2password::get, 
_name2principal::get));
-
-      return password2principal.entrySet().stream().filter(
-          entry -> BcryptUtils.checkpwWithCache(entry.getKey(), 
entry.getValue().getPassword(),
-              _userCache.getUserPasswordAuthCache())).map(u -> 
u.getValue()).filter(Objects::nonNull).findFirst();
+
+      for (String authHeader : authHeaders) {
+        String username = 
org.apache.pinot.common.auth.BasicAuthUtils.extractUsername(authHeader);
+        String password = 
org.apache.pinot.common.auth.BasicAuthUtils.extractPassword(authHeader);
+        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
+          continue;
+        }
+
+        ZkBasicAuthPrincipal principal = _name2principal.get(username);
+        if (principal == null) {
+          continue;
+        }
+
+        if (passwordMatches(principal, password)) {
+          return Optional.of(principal);
+        }
+      }
+      return Optional.empty();
+    }
+
+    private boolean passwordMatches(ZkBasicAuthPrincipal principal, String 
password) {
+      return BcryptUtils.checkpwWithCache(
+          password,
+          principal.getPassword(),
+          _userCache.getUserPasswordAuthCache());
     }
 
     @Override
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotControllerAuthResource.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotControllerAuthResource.java
index f7186cff928..2834e8695f0 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotControllerAuthResource.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotControllerAuthResource.java
@@ -68,6 +68,7 @@ public class PinotControllerAuthResource {
    *
    * @return {@code true} if authenticated and authorized, {@code false} 
otherwise
    */
+  @Deprecated
   @GET
   @Path("auth/verify")
   @Authorize(targetType = TargetType.CLUSTER, action = 
Actions.Cluster.GET_AUTH)
@@ -84,6 +85,31 @@ public class PinotControllerAuthResource {
     return accessControl.hasAccess(tableName, accessType, _httpHeaders, 
endpointUrl);
   }
 
+  /**
+   * Verify a token is both authenticated and authorized to perform an 
operation.
+   *
+   * @param tableName table name (optional)
+   * @param accessType access type (optional)
+   * @param endpointUrl endpoint url (optional)
+   *
+   * @return {@code true} if authenticated and authorized, {@code false} 
otherwise
+   */
+  @GET
+  @Path("auth/verify/v2")
+  @Authorize(targetType = TargetType.CLUSTER, action = 
Actions.Cluster.GET_AUTH)
+  @Produces(MediaType.APPLICATION_JSON)
+  @ApiOperation(value = "Check whether authentication is enabled")
+  @ApiResponses(value = {
+      @ApiResponse(code = 200, message = "Verification result provided"),
+      @ApiResponse(code = 500, message = "Verification error")
+  })
+  public boolean verifyV2(@ApiParam(value = "Table name without type") 
@QueryParam("tableName") String tableName,
+      @ApiParam(value = "API access type") @DefaultValue("READ") 
@QueryParam("accessType") AccessType accessType,
+      @ApiParam(value = "Endpoint URL") @QueryParam("endpointUrl") String 
endpointUrl) {
+    AccessControl accessControl = _accessControlFactory.create();
+    return accessControl.hasAccess(_httpHeaders, TargetType.CLUSTER);
+  }
+
   /**
    * Provide the auth workflow configuration for the Pinot UI to perform user 
authentication. Currently supports NONE
    * (no auth) and BASIC (basic auth with username and password)
diff --git a/pinot-controller/src/main/resources/app/components/Layout.tsx 
b/pinot-controller/src/main/resources/app/components/Layout.tsx
index a26d023a9b6..2805a5e63ea 100644
--- a/pinot-controller/src/main/resources/app/components/Layout.tsx
+++ b/pinot-controller/src/main/resources/app/components/Layout.tsx
@@ -31,7 +31,6 @@ import AccountCircleOutlinedIcon from 
'@material-ui/icons/AccountCircleOutlined'
 let navigationItems = [
   { id: 1, name: 'Cluster Manager', link: '/', icon: <ClusterManagerIcon /> },
   { id: 2, name: 'Query Console', link: '/query', icon: <QueryConsoleIcon /> },
-  { id: 3, name: 'Zookeeper Browser', link: '/zookeeper', icon: <ZookeeperIcon 
/> },
   { id: 4, name: 'Swagger REST API', link: 'help', target: '_blank', icon: 
<SwaggerIcon /> }
 ];
 
@@ -41,6 +40,7 @@ const Layout = (props) => {
     if(navigationItems.length <5){
       navigationItems = [
         ...navigationItems,
+        {id: 3, name: 'Zookeeper Browser', link: '/zookeeper', icon: 
<ZookeeperIcon /> },
         {id: 5, name: "User Console", link: '/user', icon: 
<AccountCircleOutlinedIcon style={{ width: 24, height: 24, verticalAlign: 'sub' 
}}/>}
       ]
     }
diff --git a/pinot-controller/src/main/resources/app/requests/index.ts 
b/pinot-controller/src/main/resources/app/requests/index.ts
index 78730c5005f..921fa9f9520 100644
--- a/pinot-controller/src/main/resources/app/requests/index.ts
+++ b/pinot-controller/src/main/resources/app/requests/index.ts
@@ -334,7 +334,7 @@ export const getInfo = (): 
Promise<AxiosResponse<OperationResponse>> =>
   baseApi.get(`/auth/info`);
 
 export const authenticateUser = (authToken): 
Promise<AxiosResponse<OperationResponse>> =>
-  baseApi.get(`/auth/verify`, {headers:{"Authorization": authToken}});
+  baseApi.get(`/auth/verify/v2`, {headers:{"Authorization": authToken}});
 
 export const getSegmentDebugInfo = (tableName: string, tableType: string): 
Promise<AxiosResponse<OperationResponse>> =>
   baseApi.get(`debug/tables/${tableName}?type=${tableType}&verbosity=10`);
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/auth/FineGrainedAccessControl.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/auth/FineGrainedAccessControl.java
index df6b51c66f3..5fe7ceb2bae 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/auth/FineGrainedAccessControl.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/auth/FineGrainedAccessControl.java
@@ -40,6 +40,17 @@ public interface FineGrainedAccessControl {
     return true;
   }
 
+  /**
+   * Checks whether the user has access to perform action on the particular 
resource type.
+   *
+   * @param httpHeaders HTTP headers
+   * @param targetType type of resource being accessed
+   * @return true if user is allowed to perform the action
+   */
+  default boolean hasAccess(HttpHeaders httpHeaders, TargetType targetType) {
+    return true;
+  }
+
   /**
    * Verifies if the user has access to perform a specific action on a 
particular resource.
    * The default implementation returns a {@link BasicAuthorizationResultImpl} 
with the result of the hasAccess() of


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

Reply via email to