You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ranger.apache.org by pr...@apache.org on 2026/08/13 11:05:52 UTC
(ranger) 01/02: RANGER-5740: Align Elasticsearch plugin caller identity with X-Pack Security
This is an automated email from the ASF dual-hosted git repository.
pradeepagrawal8184 pushed a commit to branch RANGER-5740_esplugin
in repository https://gitbox.apache.org/repos/asf/ranger.git
commit 5bedf7536f50e79e05735ae080ec5f412b59a365
Author: Pradeep Agrawal <pa...@cloudera.com>
AuthorDate: Wed Aug 12 12:53:20 2026 +0530
RANGER-5740: Align Elasticsearch plugin caller identity with X-Pack Security
---
ranger-elasticsearch-plugin-shim/pom.xml | 36 ++++++++++
.../plugin/RangerElasticsearchPlugin.java | 4 +-
.../action/filter/RangerSecurityActionFilter.java | 23 ++++++-
.../ElasticsearchAuthenticatedUserResolver.java | 56 +++++++++++++++
.../plugin/authc/user/UsernamePasswordToken.java | 8 +++
.../rest/filter/RangerSecurityRestFilter.java | 27 +++++---
...TestElasticsearchAuthenticatedUserResolver.java | 65 ++++++++++++++++++
.../rest/filter/TestRangerSecurityRestFilter.java | 79 ++++++++++++++++++++++
8 files changed, 282 insertions(+), 16 deletions(-)
diff --git a/ranger-elasticsearch-plugin-shim/pom.xml b/ranger-elasticsearch-plugin-shim/pom.xml
index 9dedb37d1..eaa25d71e 100644
--- a/ranger-elasticsearch-plugin-shim/pom.xml
+++ b/ranger-elasticsearch-plugin-shim/pom.xml
@@ -70,5 +70,41 @@
</exclusion>
</exclusions>
</dependency>
+ <dependency>
+ <groupId>org.elasticsearch.plugin</groupId>
+ <artifactId>x-pack-core</artifactId>
+ <version>${elasticsearch.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-api</artifactId>
+ <version>${log4j2.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-core</artifactId>
+ <version>${log4j2.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <version>${junit.jupiter.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <version>${mockito.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-junit-jupiter</artifactId>
+ <version>${mockito.version}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git a/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/RangerElasticsearchPlugin.java b/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/RangerElasticsearchPlugin.java
index aa1c10a30..e3b740f00 100644
--- a/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/RangerElasticsearchPlugin.java
+++ b/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/RangerElasticsearchPlugin.java
@@ -72,7 +72,7 @@ public List<ActionFilter> getActionFilters() {
@Override
public UnaryOperator<RestHandler> getRestHandlerWrapper(ThreadContext threadContext) {
- return handler -> new RangerSecurityRestFilter(threadContext, handler);
+ return handler -> new RangerSecurityRestFilter(settings, threadContext, handler);
}
@Override
@@ -81,7 +81,7 @@ public Collection<Object> createComponents(final Client client, final ClusterSer
final NamedWriteableRegistry namedWriteableRegistry, IndexNameExpressionResolver indexNameExpressionResolver, Supplier<RepositoriesService> repositoriesServiceSupplier) {
addPluginConfig2Classpath(environment);
- rangerSecurityActionFilter = new RangerSecurityActionFilter(threadPool.getThreadContext());
+ rangerSecurityActionFilter = new RangerSecurityActionFilter(settings, threadPool.getThreadContext());
return Collections.singletonList(rangerSecurityActionFilter);
}
diff --git a/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/action/filter/RangerSecurityActionFilter.java b/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/action/filter/RangerSecurityActionFilter.java
index 31af2e3a4..1d70e3b0a 100644
--- a/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/action/filter/RangerSecurityActionFilter.java
+++ b/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/action/filter/RangerSecurityActionFilter.java
@@ -19,6 +19,7 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.ranger.authorization.elasticsearch.authorizer.RangerElasticsearchAuthorizer;
+import org.apache.ranger.authorization.elasticsearch.plugin.authc.ElasticsearchAuthenticatedUserResolver;
import org.apache.ranger.authorization.elasticsearch.plugin.authc.user.UsernamePasswordToken;
import org.apache.ranger.authorization.elasticsearch.plugin.utils.RequestUtils;
import org.elasticsearch.ElasticsearchStatusException;
@@ -28,6 +29,7 @@
import org.elasticsearch.action.support.ActionFilter;
import org.elasticsearch.action.support.ActionFilterChain;
import org.elasticsearch.common.component.AbstractLifecycleComponent;
+import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.tasks.Task;
@@ -39,12 +41,14 @@
public class RangerSecurityActionFilter extends AbstractLifecycleComponent implements ActionFilter {
private static final Logger LOG = LoggerFactory.getLogger(RangerSecurityActionFilter.class);
+ private final Settings settings;
private final ThreadContext threadContext;
private final RangerElasticsearchAuthorizer rangerElasticsearchAuthorizer = new RangerElasticsearchAuthorizer();
- public RangerSecurityActionFilter(ThreadContext threadContext) {
+ public RangerSecurityActionFilter(Settings settings, ThreadContext threadContext) {
super();
+ this.settings = settings;
this.threadContext = threadContext;
}
@@ -57,7 +61,18 @@ public int order() {
public <Request extends ActionRequest, Response extends ActionResponse> void apply(Task task, String action, Request request, ActionListener<Response> listener, ActionFilterChain<Request, Response> chain) {
String user = threadContext.getTransient(UsernamePasswordToken.USERNAME);
- // If user is not null, then should check permission of the outside caller.
+ if (StringUtils.isEmpty(user)) {
+ ElasticsearchAuthenticatedUserResolver authResolver = new ElasticsearchAuthenticatedUserResolver(settings, threadContext);
+
+ if (authResolver.requiresAuthenticatedUser()) {
+ user = authResolver.resolveUsername();
+
+ if (StringUtils.isNotEmpty(user)) {
+ threadContext.putTransient(UsernamePasswordToken.USERNAME, user);
+ }
+ }
+ }
+
if (StringUtils.isNotEmpty(user)) {
List<String> indexs = RequestUtils.getIndexFromRequest(request);
String clientIPAddress = threadContext.getTransient(RequestUtils.CLIENT_IP_ADDRESS);
@@ -71,8 +86,10 @@ public <Request extends ActionRequest, Response extends ActionResponse> void app
throw new ElasticsearchStatusException(errorMsg, RestStatus.FORBIDDEN, user, action, index);
}
}
+ } else if (threadContext.isSystemContext()) {
+ LOG.debug("System context request, skipping Ranger permission check for action[{}].", action);
} else {
- LOG.debug("User is null, no check permission for elasticsearch do action[{}] with request[{}]", action, request);
+ throw new ElasticsearchStatusException("Error: Request requires authenticated user.", RestStatus.UNAUTHORIZED);
}
chain.proceed(task, action, request, listener);
diff --git a/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/authc/ElasticsearchAuthenticatedUserResolver.java b/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/authc/ElasticsearchAuthenticatedUserResolver.java
new file mode 100644
index 000000000..823d9502e
--- /dev/null
+++ b/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/authc/ElasticsearchAuthenticatedUserResolver.java
@@ -0,0 +1,56 @@
+/*
+ * 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.ranger.authorization.elasticsearch.plugin.authc;
+
+import org.apache.commons.lang3.StringUtils;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.xpack.core.security.SecurityContext;
+import org.elasticsearch.xpack.core.security.user.User;
+
+/**
+ * Resolves the Elasticsearch-verified user for the current request.
+ * Caller identity must come from X-Pack Security, not from client-supplied headers.
+ */
+public class ElasticsearchAuthenticatedUserResolver {
+ private final SecurityContext securityContext;
+ private final ThreadContext threadContext;
+
+ public ElasticsearchAuthenticatedUserResolver(Settings settings, ThreadContext threadContext) {
+ this.securityContext = new SecurityContext(settings, threadContext);
+ this.threadContext = threadContext;
+ }
+
+ public boolean requiresAuthenticatedUser() {
+ return !threadContext.isSystemContext();
+ }
+
+ public String resolveUsername() {
+ String username = null;
+
+ if (requiresAuthenticatedUser()) {
+ User user = securityContext.getUser();
+
+ if (user != null) {
+ username = user.principal();
+ }
+ }
+
+ return StringUtils.isEmpty(username) ? null : username;
+ }
+}
diff --git a/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/authc/user/UsernamePasswordToken.java b/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/authc/user/UsernamePasswordToken.java
index 958cda381..4b964f6a3 100644
--- a/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/authc/user/UsernamePasswordToken.java
+++ b/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/authc/user/UsernamePasswordToken.java
@@ -28,6 +28,10 @@
import java.util.List;
import java.util.Map;
+/**
+ * Legacy helper for parsing Basic credentials from a REST request.
+ * Parsed credentials must not be used as an authenticated identity.
+ */
public class UsernamePasswordToken {
public static final String USERNAME = "username";
public static final String BASIC_AUTH_PREFIX = "Basic ";
@@ -41,6 +45,10 @@ public UsernamePasswordToken(String username, String password) {
this.password = password;
}
+ /**
+ * @deprecated Do not use parsed Basic credentials for authorization decisions.
+ */
+ @Deprecated
public static UsernamePasswordToken parseToken(RestRequest request) {
Map<String, List<String>> headers = request.getHeaders();
diff --git a/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/rest/filter/RangerSecurityRestFilter.java b/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/rest/filter/RangerSecurityRestFilter.java
index 09b15cdc2..97a0234c4 100644
--- a/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/rest/filter/RangerSecurityRestFilter.java
+++ b/ranger-elasticsearch-plugin-shim/src/main/java/org/apache/ranger/authorization/elasticsearch/plugin/rest/filter/RangerSecurityRestFilter.java
@@ -18,11 +18,13 @@
package org.apache.ranger.authorization.elasticsearch.plugin.rest.filter;
import org.apache.commons.lang3.StringUtils;
+import org.apache.ranger.authorization.elasticsearch.plugin.authc.ElasticsearchAuthenticatedUserResolver;
import org.apache.ranger.authorization.elasticsearch.plugin.authc.user.UsernamePasswordToken;
import org.apache.ranger.authorization.elasticsearch.plugin.utils.RequestUtils;
import org.elasticsearch.ElasticsearchStatusException;
import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.component.AbstractLifecycleComponent;
+import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.rest.RestChannel;
import org.elasticsearch.rest.RestHandler;
@@ -34,29 +36,32 @@
public class RangerSecurityRestFilter extends AbstractLifecycleComponent implements RestHandler {
private static final Logger LOG = LoggerFactory.getLogger(RangerSecurityRestFilter.class);
+ private final Settings settings;
private final RestHandler restHandler;
private final ThreadContext threadContext;
- public RangerSecurityRestFilter(final ThreadContext threadContext, final RestHandler restHandler) {
+ public RangerSecurityRestFilter(final Settings settings, final ThreadContext threadContext, final RestHandler restHandler) {
super();
+ this.settings = settings;
this.restHandler = restHandler;
this.threadContext = threadContext;
}
@Override
public void handleRequest(final RestRequest request, final RestChannel channel, final NodeClient client) throws Exception {
- // Now only support to get user from request,
- // it should work with other elasticsearch identity authentication plugins in fact.
- UsernamePasswordToken user = UsernamePasswordToken.parseToken(request);
-
- if (user == null) {
- throw new ElasticsearchStatusException("Error: User is null, the request requires user authentication.", RestStatus.UNAUTHORIZED);
- } else {
- LOG.debug("Success to parse user[{}] from request[{}].", user, request);
- }
+ ElasticsearchAuthenticatedUserResolver authResolver = new ElasticsearchAuthenticatedUserResolver(settings, threadContext);
+
+ if (authResolver.requiresAuthenticatedUser()) {
+ String username = authResolver.resolveUsername();
- threadContext.putTransient(UsernamePasswordToken.USERNAME, user.getUsername());
+ if (StringUtils.isEmpty(username)) {
+ throw new ElasticsearchStatusException("Error: Request requires authenticated user.", RestStatus.UNAUTHORIZED);
+ }
+
+ threadContext.putTransient(UsernamePasswordToken.USERNAME, username);
+ LOG.debug("Using Elasticsearch-verified user[{}] for request[{}].", username, request);
+ }
String clientIPAddress = RequestUtils.getClientIPAddress(request);
diff --git a/ranger-elasticsearch-plugin-shim/src/test/java/org/apache/ranger/authorization/elasticsearch/plugin/authc/TestElasticsearchAuthenticatedUserResolver.java b/ranger-elasticsearch-plugin-shim/src/test/java/org/apache/ranger/authorization/elasticsearch/plugin/authc/TestElasticsearchAuthenticatedUserResolver.java
new file mode 100644
index 000000000..c3d0f9a42
--- /dev/null
+++ b/ranger-elasticsearch-plugin-shim/src/test/java/org/apache/ranger/authorization/elasticsearch/plugin/authc/TestElasticsearchAuthenticatedUserResolver.java
@@ -0,0 +1,65 @@
+/*
+ * 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.ranger.authorization.elasticsearch.plugin.authc;
+
+import org.elasticsearch.Version;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.xpack.core.security.SecurityContext;
+import org.elasticsearch.xpack.core.security.user.User;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestElasticsearchAuthenticatedUserResolver {
+ @Test
+ void resolveUsernameReturnsVerifiedUser() {
+ Settings settings = Settings.builder().build();
+ ThreadContext threadContext = new ThreadContext(settings);
+ SecurityContext securityContext = new SecurityContext(settings, threadContext);
+
+ securityContext.setUser(new User("admin"), Version.CURRENT);
+
+ ElasticsearchAuthenticatedUserResolver resolver = new ElasticsearchAuthenticatedUserResolver(settings, threadContext);
+
+ Assertions.assertTrue(resolver.requiresAuthenticatedUser());
+ Assertions.assertEquals("admin", resolver.resolveUsername());
+ }
+
+ @Test
+ void resolveUsernameReturnsNullWithoutVerifiedUser() {
+ Settings settings = Settings.builder().build();
+ ThreadContext threadContext = new ThreadContext(settings);
+ ElasticsearchAuthenticatedUserResolver resolver = new ElasticsearchAuthenticatedUserResolver(settings, threadContext);
+
+ Assertions.assertTrue(resolver.requiresAuthenticatedUser());
+ Assertions.assertNull(resolver.resolveUsername());
+ }
+
+ @Test
+ void systemContextDoesNotRequireAuthenticatedUser() {
+ Settings settings = Settings.builder().build();
+ ThreadContext threadContext = new ThreadContext(settings);
+
+ threadContext.markAsSystemContext();
+
+ ElasticsearchAuthenticatedUserResolver resolver = new ElasticsearchAuthenticatedUserResolver(settings, threadContext);
+
+ Assertions.assertFalse(resolver.requiresAuthenticatedUser());
+ Assertions.assertNull(resolver.resolveUsername());
+ }
+}
diff --git a/ranger-elasticsearch-plugin-shim/src/test/java/org/apache/ranger/authorization/elasticsearch/plugin/rest/filter/TestRangerSecurityRestFilter.java b/ranger-elasticsearch-plugin-shim/src/test/java/org/apache/ranger/authorization/elasticsearch/plugin/rest/filter/TestRangerSecurityRestFilter.java
new file mode 100644
index 000000000..171861bc9
--- /dev/null
+++ b/ranger-elasticsearch-plugin-shim/src/test/java/org/apache/ranger/authorization/elasticsearch/plugin/rest/filter/TestRangerSecurityRestFilter.java
@@ -0,0 +1,79 @@
+/*
+ * 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.ranger.authorization.elasticsearch.plugin.rest.filter;
+
+import org.elasticsearch.ElasticsearchStatusException;
+import org.elasticsearch.Version;
+import org.elasticsearch.client.node.NodeClient;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.http.HttpChannel;
+import org.elasticsearch.rest.RestChannel;
+import org.elasticsearch.rest.RestHandler;
+import org.elasticsearch.rest.RestRequest;
+import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.xpack.core.security.SecurityContext;
+import org.elasticsearch.xpack.core.security.user.User;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestRangerSecurityRestFilter {
+ @Test
+ void rejectsUnverifiedBasicCredentials() {
+ Settings settings = Settings.builder().build();
+ ThreadContext threadContext = new ThreadContext(settings);
+ RestHandler restHandler = Mockito.mock(RestHandler.class);
+ RestRequest request = Mockito.mock(RestRequest.class);
+ RestChannel channel = Mockito.mock(RestChannel.class);
+ NodeClient client = Mockito.mock(NodeClient.class);
+ RangerSecurityRestFilter filter = new RangerSecurityRestFilter(settings, threadContext, restHandler);
+
+ Mockito.when(request.getHeaders()).thenReturn(java.util.Collections.singletonMap(
+ "Authorization", java.util.Collections.singletonList("Basic YWRtaW46d3JvbmdwYXNzd29yZA==")));
+
+ ElasticsearchStatusException exception = Assertions.assertThrows(
+ ElasticsearchStatusException.class,
+ () -> filter.handleRequest(request, channel, client));
+
+ Assertions.assertEquals(RestStatus.UNAUTHORIZED, exception.status());
+ Mockito.verifyNoInteractions(restHandler);
+ }
+
+ @Test
+ void acceptsElasticsearchVerifiedUser() throws Exception {
+ Settings settings = Settings.builder().build();
+ ThreadContext threadContext = new ThreadContext(settings);
+ SecurityContext securityContext = new SecurityContext(settings, threadContext);
+ RestHandler restHandler = Mockito.mock(RestHandler.class);
+ RestRequest request = Mockito.mock(RestRequest.class);
+ RestChannel channel = Mockito.mock(RestChannel.class);
+ NodeClient client = Mockito.mock(NodeClient.class);
+ HttpChannel httpChannel = Mockito.mock(HttpChannel.class);
+ RangerSecurityRestFilter filter = new RangerSecurityRestFilter(settings, threadContext, restHandler);
+
+ Mockito.when(request.getHttpChannel()).thenReturn(httpChannel);
+ Mockito.when(httpChannel.getRemoteAddress()).thenReturn(null);
+
+ securityContext.setUser(new User("admin"), Version.CURRENT);
+
+ filter.handleRequest(request, channel, client);
+
+ Mockito.verify(restHandler).handleRequest(request, channel, client);
+ }
+}