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:
Albert Zaharovits 2020-08-05 15:37:37 +03:00 committed by GitHub
parent 0afa1bd972
commit e5dce5e805
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 176 additions and 4 deletions

View File

@ -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)) {

View File

@ -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");
}
}
}

View File

@ -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))

View File

@ -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),