Use the Index Access Control from the scroll search context (#60640)
When the RBACEngine authorizes scroll searches it sets the index access control to the very limiting IndicesAccessControl.ALLOW_NO_INDICES value. This change will set it to the value for the index access control that was produced during the authorization of the initial search that created the scroll, which is now stored in the scroll context.
This commit is contained in:
parent
0afa1bd972
commit
e5dce5e805
|
@ -263,10 +263,15 @@ public class RBACEngine implements AuthorizationEngine {
|
|||
if (SearchScrollAction.NAME.equals(action)) {
|
||||
authorizeIndexActionName(action, authorizationInfo, null, listener);
|
||||
} else {
|
||||
// we store the request as a transient in the ThreadContext in case of a authorization failure at the shard
|
||||
// level. If authorization fails we will audit a access_denied message and will use the request to retrieve
|
||||
// information such as the index and the incoming address of the request
|
||||
listener.onResponse(new IndexAuthorizationResult(true, IndicesAccessControl.ALLOW_NO_INDICES));
|
||||
// RBACEngine simply authorizes scroll related actions without filling in any DLS/FLS permissions.
|
||||
// Scroll related actions have special security logic, where the security context of the initial search
|
||||
// request is attached to the scroll context upon creation in {@code SecuritySearchOperationListener#onNewScrollContext}
|
||||
// and it is then verified, before every use of the scroll, in
|
||||
// {@code SecuritySearchOperationListener#validateSearchContext}.
|
||||
// The DLS/FLS permissions are used inside the {@code DirectoryReader} that {@code SecurityIndexReaderWrapper}
|
||||
// built while handling the initial search request. In addition, for consistency, the DLS/FLS permissions from
|
||||
// the originating search request are attached to the thread context upon validating the scroll.
|
||||
listener.onResponse(new IndexAuthorizationResult(true, null));
|
||||
}
|
||||
} else if (isAsyncRelatedAction(action)) {
|
||||
if (SubmitAsyncSearchAction.NAME.equals(action)) {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
package org.elasticsearch.xpack.security.authz;
|
||||
|
||||
import org.elasticsearch.ElasticsearchSecurityException;
|
||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||
import org.elasticsearch.index.shard.SearchOperationListener;
|
||||
import org.elasticsearch.license.XPackLicenseState;
|
||||
|
@ -17,6 +18,8 @@ import org.elasticsearch.xpack.core.security.SecurityContext;
|
|||
import org.elasticsearch.xpack.core.security.authc.Authentication;
|
||||
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
|
||||
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo;
|
||||
import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField;
|
||||
import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl;
|
||||
import org.elasticsearch.xpack.security.audit.AuditTrailService;
|
||||
import org.elasticsearch.xpack.security.audit.AuditUtil;
|
||||
|
||||
|
@ -51,6 +54,12 @@ public final class SecuritySearchOperationListener implements SearchOperationLis
|
|||
public void onNewScrollContext(SearchContext searchContext) {
|
||||
if (licenseState.isSecurityEnabled()) {
|
||||
searchContext.scrollContext().putInContext(AuthenticationField.AUTHENTICATION_KEY, securityContext.getAuthentication());
|
||||
// store the DLS and FLS permissions of the initial search request that created the scroll
|
||||
// this is then used to assert the DLS/FLS permission for the scroll search action
|
||||
IndicesAccessControl indicesAccessControl =
|
||||
securityContext.getThreadContext().getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY);
|
||||
assert indicesAccessControl != null : "thread context does not contain index access control";
|
||||
searchContext.scrollContext().putInContext(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, indicesAccessControl);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,6 +77,39 @@ public final class SecuritySearchOperationListener implements SearchOperationLis
|
|||
final String action = threadContext.getTransient(ORIGINATING_ACTION_KEY);
|
||||
ensureAuthenticatedUserIsSame(originalAuth, current, auditTrailService, searchContext.id(), action, request,
|
||||
AuditUtil.extractRequestId(threadContext), threadContext.getTransient(AUTHORIZATION_INFO_KEY));
|
||||
// piggyback on context validation to assert the DLS/FLS permissions on the thread context of the scroll search handler
|
||||
if (null == securityContext.getThreadContext().getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY)) {
|
||||
// fill in the DLS and FLS permissions for the scroll search action from the scroll context
|
||||
IndicesAccessControl scrollIndicesAccessControl =
|
||||
searchContext.scrollContext().getFromContext(AuthorizationServiceField.INDICES_PERMISSIONS_KEY);
|
||||
assert scrollIndicesAccessControl != null : "scroll does not contain index access control";
|
||||
securityContext.getThreadContext().putTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY,
|
||||
scrollIndicesAccessControl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreFetchPhase(SearchContext searchContext) {
|
||||
ensureIndicesAccessControlForScrollThreadContext(searchContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreQueryPhase(SearchContext searchContext) {
|
||||
ensureIndicesAccessControlForScrollThreadContext(searchContext);
|
||||
}
|
||||
|
||||
void ensureIndicesAccessControlForScrollThreadContext(SearchContext searchContext) {
|
||||
if (licenseState.isSecurityEnabled() && searchContext.scrollContext() != null) {
|
||||
IndicesAccessControl scrollIndicesAccessControl =
|
||||
searchContext.scrollContext().getFromContext(AuthorizationServiceField.INDICES_PERMISSIONS_KEY);
|
||||
IndicesAccessControl threadIndicesAccessControl =
|
||||
securityContext.getThreadContext().getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY);
|
||||
if (scrollIndicesAccessControl != threadIndicesAccessControl) {
|
||||
throw new ElasticsearchSecurityException("[" + searchContext.id() + "] expected scroll indices access control [" +
|
||||
scrollIndicesAccessControl.toString() + "] but found [" + threadIndicesAccessControl.toString() + "] in thread " +
|
||||
"context");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.elasticsearch.common.unit.TimeValue;
|
|||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentFactory;
|
||||
import org.elasticsearch.index.IndexModule;
|
||||
import org.elasticsearch.index.query.QueryBuilder;
|
||||
import org.elasticsearch.index.query.QueryBuilders;
|
||||
import org.elasticsearch.indices.IndicesRequestCache;
|
||||
import org.elasticsearch.join.ParentJoinPlugin;
|
||||
|
@ -768,6 +769,114 @@ public class FieldLevelSecurityTests extends SecurityIntegTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
public void testScrollWithQueryCache() {
|
||||
assertAcked(client().admin().indices().prepareCreate("test")
|
||||
.setSettings(Settings.builder().put(IndexModule.INDEX_QUERY_CACHE_EVERYTHING_SETTING.getKey(), true))
|
||||
.addMapping("type1", "field1", "type=text", "field2", "type=text")
|
||||
);
|
||||
|
||||
final int numDocs = scaledRandomIntBetween(2, 4);
|
||||
for (int i = 0; i < numDocs; i++) {
|
||||
client().prepareIndex("test", "type1", String.valueOf(i))
|
||||
.setSource("field1", "value1", "field2", "value2")
|
||||
.get();
|
||||
}
|
||||
refresh("test");
|
||||
|
||||
final QueryBuilder cacheableQueryBuilder = constantScoreQuery(termQuery("field1", "value1"));
|
||||
|
||||
SearchResponse user1SearchResponse = null;
|
||||
SearchResponse user2SearchResponse = null;
|
||||
int scrolledDocsUser1 = 0;
|
||||
final int numScrollSearch = scaledRandomIntBetween(20, 30);
|
||||
|
||||
try {
|
||||
for (int i = 0; i < numScrollSearch; i++) {
|
||||
if (randomBoolean()) {
|
||||
if (user2SearchResponse == null) {
|
||||
user2SearchResponse = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue(
|
||||
"user2", USERS_PASSWD)))
|
||||
.prepareSearch("test")
|
||||
.setQuery(cacheableQueryBuilder)
|
||||
.setScroll(TimeValue.timeValueMinutes(10L))
|
||||
.setSize(1)
|
||||
.setFetchSource(true)
|
||||
.get();
|
||||
assertThat(user2SearchResponse.getHits().getTotalHits().value, is((long) 0));
|
||||
assertThat(user2SearchResponse.getHits().getHits().length, is(0));
|
||||
} else {
|
||||
// make sure scroll is empty
|
||||
user2SearchResponse = client()
|
||||
.filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user2",
|
||||
USERS_PASSWD)))
|
||||
.prepareSearchScroll(user2SearchResponse.getScrollId())
|
||||
.setScroll(TimeValue.timeValueMinutes(10L))
|
||||
.get();
|
||||
assertThat(user2SearchResponse.getHits().getTotalHits().value, is((long) 0));
|
||||
assertThat(user2SearchResponse.getHits().getHits().length, is(0));
|
||||
if (randomBoolean()) {
|
||||
// maybe reuse the scroll even if empty
|
||||
client().prepareClearScroll().addScrollId(user2SearchResponse.getScrollId()).get();
|
||||
user2SearchResponse = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (user1SearchResponse == null) {
|
||||
user1SearchResponse = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue(
|
||||
"user1", USERS_PASSWD)))
|
||||
.prepareSearch("test")
|
||||
.setQuery(cacheableQueryBuilder)
|
||||
.setScroll(TimeValue.timeValueMinutes(10L))
|
||||
.setSize(1)
|
||||
.setFetchSource(true)
|
||||
.get();
|
||||
assertThat(user1SearchResponse.getHits().getTotalHits().value, is((long) numDocs));
|
||||
assertThat(user1SearchResponse.getHits().getHits().length, is(1));
|
||||
assertThat(user1SearchResponse.getHits().getAt(0).getSourceAsMap().size(), is(1));
|
||||
assertThat(user1SearchResponse.getHits().getAt(0).getSourceAsMap().get("field1"), is("value1"));
|
||||
scrolledDocsUser1++;
|
||||
} else {
|
||||
user1SearchResponse = client()
|
||||
.filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user1", USERS_PASSWD)))
|
||||
.prepareSearchScroll(user1SearchResponse.getScrollId())
|
||||
.setScroll(TimeValue.timeValueMinutes(10L))
|
||||
.get();
|
||||
assertThat(user1SearchResponse.getHits().getTotalHits().value, is((long) numDocs));
|
||||
if (scrolledDocsUser1 < numDocs) {
|
||||
assertThat(user1SearchResponse.getHits().getHits().length, is(1));
|
||||
assertThat(user1SearchResponse.getHits().getAt(0).getSourceAsMap().size(), is(1));
|
||||
assertThat(user1SearchResponse.getHits().getAt(0).getSourceAsMap().get("field1"), is("value1"));
|
||||
scrolledDocsUser1++;
|
||||
} else {
|
||||
assertThat(user1SearchResponse.getHits().getHits().length, is(0));
|
||||
if (randomBoolean()) {
|
||||
// maybe reuse the scroll even if empty
|
||||
if (user1SearchResponse.getScrollId() != null) {
|
||||
client().prepareClearScroll().addScrollId(user1SearchResponse.getScrollId()).get();
|
||||
}
|
||||
user1SearchResponse = null;
|
||||
scrolledDocsUser1 = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (user1SearchResponse != null) {
|
||||
String scrollId = user1SearchResponse.getScrollId();
|
||||
if (scrollId != null) {
|
||||
client().prepareClearScroll().addScrollId(scrollId).get();
|
||||
}
|
||||
}
|
||||
if (user2SearchResponse != null) {
|
||||
String scrollId = user2SearchResponse.getScrollId();
|
||||
if (scrollId != null) {
|
||||
client().prepareClearScroll().addScrollId(scrollId).get();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void testRequestCache() throws Exception {
|
||||
assertAcked(client().admin().indices().prepareCreate("test")
|
||||
.setSettings(Settings.builder().put(IndicesRequestCache.INDEX_CACHE_REQUEST_ENABLED_SETTING.getKey(), true))
|
||||
|
|
|
@ -27,6 +27,8 @@ import org.elasticsearch.xpack.core.security.authc.Authentication;
|
|||
import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef;
|
||||
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
|
||||
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo;
|
||||
import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField;
|
||||
import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl;
|
||||
import org.elasticsearch.xpack.core.security.user.User;
|
||||
import org.elasticsearch.xpack.security.audit.AuditTrail;
|
||||
import org.elasticsearch.xpack.security.audit.AuditTrailService;
|
||||
|
@ -39,6 +41,8 @@ import static org.elasticsearch.xpack.security.authz.AuthorizationService.AUTHOR
|
|||
import static org.elasticsearch.xpack.security.authz.AuthorizationService.ORIGINATING_ACTION_KEY;
|
||||
import static org.elasticsearch.xpack.security.authz.AuthorizationServiceTests.authzInfoRoles;
|
||||
import static org.elasticsearch.xpack.security.authz.SecuritySearchOperationListener.ensureAuthenticatedUserIsSame;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
|
@ -77,6 +81,8 @@ public class SecuritySearchOperationListenerTests extends ESTestCase {
|
|||
AuditTrailService auditTrailService = mock(AuditTrailService.class);
|
||||
Authentication authentication = new Authentication(new User("test", "role"), new RealmRef("realm", "file", "node"), null);
|
||||
authentication.writeToContext(threadContext);
|
||||
IndicesAccessControl indicesAccessControl = mock(IndicesAccessControl.class);
|
||||
threadContext.putTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, indicesAccessControl);
|
||||
|
||||
SecuritySearchOperationListener listener = new SecuritySearchOperationListener(securityContext, licenseState, auditTrailService);
|
||||
listener.onNewScrollContext(testSearchContext);
|
||||
|
@ -85,6 +91,9 @@ public class SecuritySearchOperationListenerTests extends ESTestCase {
|
|||
assertEquals(authentication, contextAuth);
|
||||
assertEquals(scroll, testSearchContext.scrollContext().scroll);
|
||||
|
||||
assertThat(testSearchContext.scrollContext().getFromContext(AuthorizationServiceField.INDICES_PERMISSIONS_KEY),
|
||||
is(indicesAccessControl));
|
||||
|
||||
verify(licenseState).isSecurityEnabled();
|
||||
verifyZeroInteractions(auditTrailService);
|
||||
}
|
||||
|
@ -94,6 +103,8 @@ public class SecuritySearchOperationListenerTests extends ESTestCase {
|
|||
testSearchContext.scrollContext(new ScrollContext());
|
||||
testSearchContext.scrollContext().putInContext(AuthenticationField.AUTHENTICATION_KEY,
|
||||
new Authentication(new User("test", "role"), new RealmRef("realm", "file", "node"), null));
|
||||
final IndicesAccessControl indicesAccessControl = mock(IndicesAccessControl.class);
|
||||
testSearchContext.scrollContext().putInContext(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, indicesAccessControl);
|
||||
testSearchContext.scrollContext().scroll = new Scroll(TimeValue.timeValueSeconds(2L));
|
||||
XPackLicenseState licenseState = mock(XPackLicenseState.class);
|
||||
when(licenseState.isSecurityEnabled()).thenReturn(true);
|
||||
|
@ -108,6 +119,7 @@ public class SecuritySearchOperationListenerTests extends ESTestCase {
|
|||
Authentication authentication = new Authentication(new User("test", "role"), new RealmRef("realm", "file", "node"), null);
|
||||
authentication.writeToContext(threadContext);
|
||||
listener.validateSearchContext(testSearchContext, Empty.INSTANCE);
|
||||
assertThat(threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY), is(indicesAccessControl));
|
||||
verify(licenseState).isSecurityEnabled();
|
||||
verifyZeroInteractions(auditTrail);
|
||||
}
|
||||
|
@ -118,6 +130,7 @@ public class SecuritySearchOperationListenerTests extends ESTestCase {
|
|||
Authentication authentication = new Authentication(new User("test", "role"), new RealmRef(realmName, "file", nodeName), null);
|
||||
authentication.writeToContext(threadContext);
|
||||
listener.validateSearchContext(testSearchContext, Empty.INSTANCE);
|
||||
assertThat(threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY), is(indicesAccessControl));
|
||||
verify(licenseState, times(2)).isSecurityEnabled();
|
||||
verifyZeroInteractions(auditTrail);
|
||||
}
|
||||
|
@ -134,6 +147,7 @@ public class SecuritySearchOperationListenerTests extends ESTestCase {
|
|||
final InternalScrollSearchRequest request = new InternalScrollSearchRequest();
|
||||
SearchContextMissingException expected =
|
||||
expectThrows(SearchContextMissingException.class, () -> listener.validateSearchContext(testSearchContext, request));
|
||||
assertThat(threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY), nullValue());
|
||||
assertEquals(testSearchContext.id(), expected.contextId());
|
||||
verify(licenseState, Mockito.atLeast(3)).isSecurityEnabled();
|
||||
verify(auditTrail).accessDenied(eq(null), eq(authentication), eq("action"), eq(request),
|
||||
|
@ -152,6 +166,7 @@ public class SecuritySearchOperationListenerTests extends ESTestCase {
|
|||
threadContext.putTransient(ORIGINATING_ACTION_KEY, "action");
|
||||
final InternalScrollSearchRequest request = new InternalScrollSearchRequest();
|
||||
listener.validateSearchContext(testSearchContext, request);
|
||||
assertThat(threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY), is(indicesAccessControl));
|
||||
verify(licenseState, Mockito.atLeast(4)).isSecurityEnabled();
|
||||
verifyNoMoreInteractions(auditTrail);
|
||||
}
|
||||
|
@ -170,6 +185,7 @@ public class SecuritySearchOperationListenerTests extends ESTestCase {
|
|||
final InternalScrollSearchRequest request = new InternalScrollSearchRequest();
|
||||
SearchContextMissingException expected =
|
||||
expectThrows(SearchContextMissingException.class, () -> listener.validateSearchContext(testSearchContext, request));
|
||||
assertThat(threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY), nullValue());
|
||||
assertEquals(testSearchContext.id(), expected.contextId());
|
||||
verify(licenseState, Mockito.atLeast(5)).isSecurityEnabled();
|
||||
verify(auditTrail).accessDenied(eq(null), eq(authentication), eq("action"), eq(request),
|
||||
|
|
Loading…
Reference in New Issue