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]