Skip to content

Commit 8dbcd49

Browse files
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.
1 parent e38ae41 commit 8dbcd49

File tree

4 files changed

+175
-7
lines changed

4 files changed

+175
-7
lines changed

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,15 @@ public void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo auth
239239
if (SearchScrollAction.NAME.equals(action)) {
240240
authorizeIndexActionName(action, authorizationInfo, null, listener);
241241
} else {
242-
// we store the request as a transient in the ThreadContext in case of a authorization failure at the shard
243-
// level. If authorization fails we will audit a access_denied message and will use the request to retrieve
244-
// information such as the index and the incoming address of the request
245-
listener.onResponse(new IndexAuthorizationResult(true, IndicesAccessControl.ALLOW_NO_INDICES));
242+
// RBACEngine simply authorizes scroll related actions without filling in any DLS/FLS permissions.
243+
// Scroll related actions have special security logic, where the security context of the initial search
244+
// request is attached to the scroll context upon creation in {@code SecuritySearchOperationListener#onNewScrollContext}
245+
// and it is then verified, before every use of the scroll, in
246+
// {@code SecuritySearchOperationListener#validateSearchContext}.
247+
// The DLS/FLS permissions are used inside the {@code DirectoryReader} that {@code SecurityIndexReaderWrapper}
248+
// built while handling the initial search request. In addition, for consistency, the DLS/FLS permissions from
249+
// the originating search request are attached to the thread context upon validating the scroll.
250+
listener.onResponse(new IndexAuthorizationResult(true, null));
246251
}
247252
} else {
248253
assert false :

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListener.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,21 @@
55
*/
66
package org.elasticsearch.xpack.security.authz;
77

8+
import org.elasticsearch.ElasticsearchSecurityException;
89
import org.elasticsearch.common.util.concurrent.ThreadContext;
910
import org.elasticsearch.index.shard.SearchOperationListener;
1011
import org.elasticsearch.license.XPackLicenseState;
1112
import org.elasticsearch.search.SearchContextMissingException;
1213
import org.elasticsearch.search.internal.ScrollContext;
1314
import org.elasticsearch.search.internal.SearchContext;
1415
import org.elasticsearch.transport.TransportRequest;
15-
import org.elasticsearch.xpack.security.audit.AuditTrailService;
1616
import org.elasticsearch.xpack.core.security.authc.Authentication;
1717
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
18-
import org.elasticsearch.xpack.security.audit.AuditUtil;
1918
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo;
19+
import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField;
20+
import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl;
21+
import org.elasticsearch.xpack.security.audit.AuditTrailService;
22+
import org.elasticsearch.xpack.security.audit.AuditUtil;
2023

2124
import static org.elasticsearch.xpack.security.authz.AuthorizationService.AUTHORIZATION_INFO_KEY;
2225
import static org.elasticsearch.xpack.security.authz.AuthorizationService.ORIGINATING_ACTION_KEY;
@@ -50,6 +53,9 @@ public void onNewScrollContext(SearchContext searchContext) {
5053
if (licenseState.isAuthAllowed()) {
5154
searchContext.scrollContext().putInContext(AuthenticationField.AUTHENTICATION_KEY,
5255
Authentication.getAuthentication(threadContext));
56+
IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY);
57+
assert indicesAccessControl != null : "thread context does not contain index access control";
58+
searchContext.scrollContext().putInContext(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, indicesAccessControl);
5359
}
5460
}
5561

@@ -66,6 +72,38 @@ public void validateSearchContext(SearchContext searchContext, TransportRequest
6672
final String action = threadContext.getTransient(ORIGINATING_ACTION_KEY);
6773
ensureAuthenticatedUserIsSame(originalAuth, current, auditTrailService, searchContext.id(), action, request,
6874
AuditUtil.extractRequestId(threadContext), threadContext.getTransient(AUTHORIZATION_INFO_KEY));
75+
// piggyback on context validation to assert the DLS/FLS permissions on the thread context of the scroll search handler
76+
if (null == threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY)) {
77+
// fill in the DLS and FLS permissions for the scroll search action from the scroll context
78+
IndicesAccessControl scrollIndicesAccessControl =
79+
searchContext.scrollContext().getFromContext(AuthorizationServiceField.INDICES_PERMISSIONS_KEY);
80+
assert scrollIndicesAccessControl != null : "scroll does not contain index access control";
81+
threadContext.putTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, scrollIndicesAccessControl);
82+
}
83+
}
84+
}
85+
}
86+
87+
@Override
88+
public void onPreFetchPhase(SearchContext searchContext) {
89+
ensureIndicesAccessControlForScrollThreadContext(searchContext);
90+
}
91+
92+
@Override
93+
public void onPreQueryPhase(SearchContext searchContext) {
94+
ensureIndicesAccessControlForScrollThreadContext(searchContext);
95+
}
96+
97+
void ensureIndicesAccessControlForScrollThreadContext(SearchContext searchContext) {
98+
if (licenseState.isAuthAllowed() && searchContext.scrollContext() != null) {
99+
IndicesAccessControl scrollIndicesAccessControl =
100+
searchContext.scrollContext().getFromContext(AuthorizationServiceField.INDICES_PERMISSIONS_KEY);
101+
IndicesAccessControl threadIndicesAccessControl =
102+
threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY);
103+
if (scrollIndicesAccessControl != threadIndicesAccessControl) {
104+
throw new ElasticsearchSecurityException("[" + searchContext.id() + "] expected scroll indices access control [" +
105+
scrollIndicesAccessControl.toString() + "] but found [" + threadIndicesAccessControl.toString() + "] in thread " +
106+
"context");
69107
}
70108
}
71109
}

x-pack/plugin/security/src/test/java/org/elasticsearch/integration/FieldLevelSecurityTests.java

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.elasticsearch.common.xcontent.XContentBuilder;
2727
import org.elasticsearch.common.xcontent.XContentFactory;
2828
import org.elasticsearch.index.IndexModule;
29+
import org.elasticsearch.index.query.QueryBuilder;
2930
import org.elasticsearch.index.query.QueryBuilders;
3031
import org.elasticsearch.indices.IndicesRequestCache;
3132
import org.elasticsearch.join.ParentJoinPlugin;
@@ -768,6 +769,114 @@ public void testQueryCache() throws Exception {
768769
}
769770
}
770771

772+
public void testScrollWithQueryCache() {
773+
assertAcked(client().admin().indices().prepareCreate("test")
774+
.setSettings(Settings.builder().put(IndexModule.INDEX_QUERY_CACHE_EVERYTHING_SETTING.getKey(), true))
775+
.addMapping("type1", "field1", "type=text", "field2", "type=text")
776+
);
777+
778+
final int numDocs = scaledRandomIntBetween(2, 4);
779+
for (int i = 0; i < numDocs; i++) {
780+
client().prepareIndex("test", "type1", String.valueOf(i))
781+
.setSource("field1", "value1", "field2", "value2")
782+
.get();
783+
}
784+
refresh("test");
785+
786+
final QueryBuilder cacheableQueryBuilder = constantScoreQuery(termQuery("field1", "value1"));
787+
788+
SearchResponse user1SearchResponse = null;
789+
SearchResponse user2SearchResponse = null;
790+
int scrolledDocsUser1 = 0;
791+
final int numScrollSearch = scaledRandomIntBetween(20, 30);
792+
793+
try {
794+
for (int i = 0; i < numScrollSearch; i++) {
795+
if (randomBoolean()) {
796+
if (user2SearchResponse == null) {
797+
user2SearchResponse = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue(
798+
"user2", USERS_PASSWD)))
799+
.prepareSearch("test")
800+
.setQuery(cacheableQueryBuilder)
801+
.setScroll(TimeValue.timeValueMinutes(10L))
802+
.setSize(1)
803+
.setFetchSource(true)
804+
.get();
805+
assertThat(user2SearchResponse.getHits().getTotalHits(), is((long) 0));
806+
assertThat(user2SearchResponse.getHits().getHits().length, is(0));
807+
} else {
808+
// make sure scroll is empty
809+
user2SearchResponse = client()
810+
.filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user2",
811+
USERS_PASSWD)))
812+
.prepareSearchScroll(user2SearchResponse.getScrollId())
813+
.setScroll(TimeValue.timeValueMinutes(10L))
814+
.get();
815+
assertThat(user2SearchResponse.getHits().getTotalHits(), is((long) 0));
816+
assertThat(user2SearchResponse.getHits().getHits().length, is(0));
817+
if (randomBoolean()) {
818+
// maybe reuse the scroll even if empty
819+
client().prepareClearScroll().addScrollId(user2SearchResponse.getScrollId()).get();
820+
user2SearchResponse = null;
821+
}
822+
}
823+
} else {
824+
if (user1SearchResponse == null) {
825+
user1SearchResponse = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue(
826+
"user1", USERS_PASSWD)))
827+
.prepareSearch("test")
828+
.setQuery(cacheableQueryBuilder)
829+
.setScroll(TimeValue.timeValueMinutes(10L))
830+
.setSize(1)
831+
.setFetchSource(true)
832+
.get();
833+
assertThat(user1SearchResponse.getHits().getTotalHits(), is((long) numDocs));
834+
assertThat(user1SearchResponse.getHits().getHits().length, is(1));
835+
assertThat(user1SearchResponse.getHits().getAt(0).getSourceAsMap().size(), is(1));
836+
assertThat(user1SearchResponse.getHits().getAt(0).getSourceAsMap().get("field1"), is("value1"));
837+
scrolledDocsUser1++;
838+
} else {
839+
user1SearchResponse = client()
840+
.filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user1", USERS_PASSWD)))
841+
.prepareSearchScroll(user1SearchResponse.getScrollId())
842+
.setScroll(TimeValue.timeValueMinutes(10L))
843+
.get();
844+
assertThat(user1SearchResponse.getHits().getTotalHits(), is((long) numDocs));
845+
if (scrolledDocsUser1 < numDocs) {
846+
assertThat(user1SearchResponse.getHits().getHits().length, is(1));
847+
assertThat(user1SearchResponse.getHits().getAt(0).getSourceAsMap().size(), is(1));
848+
assertThat(user1SearchResponse.getHits().getAt(0).getSourceAsMap().get("field1"), is("value1"));
849+
scrolledDocsUser1++;
850+
} else {
851+
assertThat(user1SearchResponse.getHits().getHits().length, is(0));
852+
if (randomBoolean()) {
853+
// maybe reuse the scroll even if empty
854+
if (user1SearchResponse.getScrollId() != null) {
855+
client().prepareClearScroll().addScrollId(user1SearchResponse.getScrollId()).get();
856+
}
857+
user1SearchResponse = null;
858+
scrolledDocsUser1 = 0;
859+
}
860+
}
861+
}
862+
}
863+
}
864+
} finally {
865+
if (user1SearchResponse != null) {
866+
String scrollId = user1SearchResponse.getScrollId();
867+
if (scrollId != null) {
868+
client().prepareClearScroll().addScrollId(scrollId).get();
869+
}
870+
}
871+
if (user2SearchResponse != null) {
872+
String scrollId = user2SearchResponse.getScrollId();
873+
if (scrollId != null) {
874+
client().prepareClearScroll().addScrollId(scrollId).get();
875+
}
876+
}
877+
}
878+
}
879+
771880
public void testRequestCache() throws Exception {
772881
assertAcked(client().admin().indices().prepareCreate("test")
773882
.setSettings(Settings.builder().put(IndicesRequestCache.INDEX_CACHE_REQUEST_ENABLED_SETTING.getKey(), true))

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListenerTests.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
import org.elasticsearch.xpack.core.security.authc.Authentication;
2323
import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef;
2424
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
25+
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo;
26+
import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField;
27+
import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl;
2528
import org.elasticsearch.xpack.core.security.user.User;
2629
import org.elasticsearch.xpack.security.audit.AuditTrailService;
27-
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo;
2830

2931
import java.util.Collections;
3032

@@ -34,6 +36,8 @@
3436
import static org.elasticsearch.xpack.security.authz.AuthorizationService.ORIGINATING_ACTION_KEY;
3537
import static org.elasticsearch.xpack.security.authz.AuthorizationServiceTests.authzInfoRoles;
3638
import static org.elasticsearch.xpack.security.authz.SecuritySearchOperationListener.ensureAuthenticatedUserIsSame;
39+
import static org.hamcrest.Matchers.is;
40+
import static org.hamcrest.Matchers.nullValue;
3741
import static org.mockito.Matchers.eq;
3842
import static org.mockito.Mockito.mock;
3943
import static org.mockito.Mockito.times;
@@ -69,6 +73,8 @@ public void testOnNewContextSetsAuthentication() throws Exception {
6973
AuditTrailService auditTrailService = mock(AuditTrailService.class);
7074
Authentication authentication = new Authentication(new User("test", "role"), new RealmRef("realm", "file", "node"), null);
7175
authentication.writeToContext(threadContext);
76+
IndicesAccessControl indicesAccessControl = mock(IndicesAccessControl.class);
77+
threadContext.putTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, indicesAccessControl);
7278

7379
SecuritySearchOperationListener listener = new SecuritySearchOperationListener(threadContext, licenseState, auditTrailService);
7480
listener.onNewScrollContext(testSearchContext);
@@ -77,6 +83,9 @@ public void testOnNewContextSetsAuthentication() throws Exception {
7783
assertEquals(authentication, contextAuth);
7884
assertEquals(scroll, testSearchContext.scrollContext().scroll);
7985

86+
assertThat(testSearchContext.scrollContext().getFromContext(AuthorizationServiceField.INDICES_PERMISSIONS_KEY),
87+
is(indicesAccessControl));
88+
8089
verify(licenseState).isAuthAllowed();
8190
verifyZeroInteractions(auditTrailService);
8291
}
@@ -86,6 +95,8 @@ public void testValidateSearchContext() throws Exception {
8695
testSearchContext.scrollContext(new ScrollContext());
8796
testSearchContext.scrollContext().putInContext(AuthenticationField.AUTHENTICATION_KEY,
8897
new Authentication(new User("test", "role"), new RealmRef("realm", "file", "node"), null));
98+
final IndicesAccessControl indicesAccessControl = mock(IndicesAccessControl.class);
99+
testSearchContext.scrollContext().putInContext(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, indicesAccessControl);
89100
testSearchContext.scrollContext().scroll = new Scroll(TimeValue.timeValueSeconds(2L));
90101
XPackLicenseState licenseState = mock(XPackLicenseState.class);
91102
when(licenseState.isAuthAllowed()).thenReturn(true);
@@ -97,6 +108,7 @@ public void testValidateSearchContext() throws Exception {
97108
Authentication authentication = new Authentication(new User("test", "role"), new RealmRef("realm", "file", "node"), null);
98109
authentication.writeToContext(threadContext);
99110
listener.validateSearchContext(testSearchContext, Empty.INSTANCE);
111+
assertThat(threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY), is(indicesAccessControl));
100112
verify(licenseState).isAuthAllowed();
101113
verifyZeroInteractions(auditTrailService);
102114
}
@@ -107,6 +119,7 @@ public void testValidateSearchContext() throws Exception {
107119
Authentication authentication = new Authentication(new User("test", "role"), new RealmRef(realmName, "file", nodeName), null);
108120
authentication.writeToContext(threadContext);
109121
listener.validateSearchContext(testSearchContext, Empty.INSTANCE);
122+
assertThat(threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY), is(indicesAccessControl));
110123
verify(licenseState, times(2)).isAuthAllowed();
111124
verifyZeroInteractions(auditTrailService);
112125
}
@@ -123,6 +136,7 @@ public void testValidateSearchContext() throws Exception {
123136
final InternalScrollSearchRequest request = new InternalScrollSearchRequest();
124137
SearchContextMissingException expected =
125138
expectThrows(SearchContextMissingException.class, () -> listener.validateSearchContext(testSearchContext, request));
139+
assertThat(threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY), nullValue());
126140
assertEquals(testSearchContext.id(), expected.id());
127141
verify(licenseState, times(3)).isAuthAllowed();
128142
verify(auditTrailService).accessDenied(eq(null), eq(authentication), eq("action"), eq(request),
@@ -141,6 +155,7 @@ public void testValidateSearchContext() throws Exception {
141155
threadContext.putTransient(ORIGINATING_ACTION_KEY, "action");
142156
final InternalScrollSearchRequest request = new InternalScrollSearchRequest();
143157
listener.validateSearchContext(testSearchContext, request);
158+
assertThat(threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY), is(indicesAccessControl));
144159
verify(licenseState, times(4)).isAuthAllowed();
145160
verifyNoMoreInteractions(auditTrailService);
146161
}
@@ -159,6 +174,7 @@ public void testValidateSearchContext() throws Exception {
159174
final InternalScrollSearchRequest request = new InternalScrollSearchRequest();
160175
SearchContextMissingException expected =
161176
expectThrows(SearchContextMissingException.class, () -> listener.validateSearchContext(testSearchContext, request));
177+
assertThat(threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY), nullValue());
162178
assertEquals(testSearchContext.id(), expected.id());
163179
verify(licenseState, times(5)).isAuthAllowed();
164180
verify(auditTrailService).accessDenied(eq(null), eq(authentication), eq("action"), eq(request),

0 commit comments

Comments
 (0)