Skip to content

Commit 2cc663d

Browse files
authored
put mapping authorization for alias with write-index and multiple read indices (#40834) (#41289)
When the same alias points to multiple indices we can write to only one index with `is_write_index` value `true`. The special handling in case of the put mapping request(to resolve authorized indices) has a check on indices size for a concrete index. If multiple indices existed then it marked the request as unauthorized. The check has been modified to consider write index flag and only when the requested index matches with the one with write index alias, the alias is considered for authorization. Closes #40831
1 parent 3c88dbf commit 2cc663d

File tree

3 files changed

+158
-6
lines changed

3 files changed

+158
-6
lines changed

x-pack/plugin/ilm/qa/with-security/src/test/java/org/elasticsearch/xpack/security/PermissionsIT.java

Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77

88
import org.apache.http.entity.ContentType;
99
import org.apache.http.entity.StringEntity;
10+
import org.elasticsearch.client.Node;
1011
import org.elasticsearch.client.Request;
1112
import org.elasticsearch.client.Response;
1213
import org.elasticsearch.client.ResponseException;
14+
import org.elasticsearch.client.RestClient;
15+
import org.elasticsearch.client.RestClientBuilder;
1316
import org.elasticsearch.common.Strings;
1417
import org.elasticsearch.common.settings.SecureString;
1518
import org.elasticsearch.common.settings.Settings;
@@ -19,13 +22,15 @@
1922
import org.elasticsearch.common.xcontent.XContentHelper;
2023
import org.elasticsearch.common.xcontent.XContentType;
2124
import org.elasticsearch.common.xcontent.json.JsonXContent;
25+
import org.elasticsearch.common.xcontent.support.XContentMapValues;
2226
import org.elasticsearch.rest.RestStatus;
2327
import org.elasticsearch.test.rest.ESRestTestCase;
2428
import org.elasticsearch.xpack.core.indexlifecycle.DeleteAction;
2529
import org.elasticsearch.xpack.core.indexlifecycle.LifecycleAction;
2630
import org.elasticsearch.xpack.core.indexlifecycle.LifecyclePolicy;
2731
import org.elasticsearch.xpack.core.indexlifecycle.LifecycleSettings;
2832
import org.elasticsearch.xpack.core.indexlifecycle.Phase;
33+
import org.elasticsearch.xpack.core.indexlifecycle.RolloverAction;
2934
import org.junit.Before;
3035

3136
import java.io.IOException;
@@ -36,8 +41,10 @@
3641
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
3742
import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
3843
import static org.hamcrest.Matchers.equalTo;
44+
import static org.hamcrest.Matchers.is;
3945

4046
public class PermissionsIT extends ESRestTestCase {
47+
private static final String jsonDoc = "{ \"name\" : \"elasticsearch\", \"body\": \"foo bar\" }";
4148

4249
private String deletePolicy = "deletePolicy";
4350
private Settings indexSettingsWithPolicy;
@@ -74,7 +81,7 @@ public void init() throws Exception {
7481
.put("number_of_shards", 1)
7582
.put("number_of_replicas", 0)
7683
.build();
77-
createNewSingletonPolicy(deletePolicy,"delete", new DeleteAction());
84+
createNewSingletonPolicy(client(), deletePolicy,"delete", new DeleteAction());
7885
}
7986

8087
/**
@@ -126,7 +133,62 @@ public void testCanViewExplainOnUnmanagedIndex() throws Exception {
126133
assertOK(client().performRequest(request));
127134
}
128135

129-
private void createNewSingletonPolicy(String policy, String phaseName, LifecycleAction action) throws IOException {
136+
/**
137+
* Tests when the user is limited by alias of an index is able to write to index
138+
* which was rolled over by an ILM policy.
139+
*/
140+
public void testWhenUserLimitedByOnlyAliasOfIndexCanWriteToIndexWhichWasRolledoverByILMPolicy()
141+
throws IOException, InterruptedException {
142+
/*
143+
* Setup:
144+
* - ILM policy to rollover index when max docs condition is met
145+
* - Index template to which the ILM policy applies and create Index
146+
* - Create role with just write and manage privileges on alias
147+
* - Create user and assign newly created role.
148+
*/
149+
createNewSingletonPolicy(adminClient(), "foo-policy", "hot", new RolloverAction(null, null, 2L));
150+
createIndexTemplate("foo-template", "foo-logs-*", "foo_alias", "foo-policy");
151+
createIndexAsAdmin("foo-logs-000001", "foo_alias", randomBoolean());
152+
createRole("foo_alias_role", "foo_alias");
153+
createUser("test_user", "x-pack-test-password", "foo_alias_role");
154+
155+
// test_user: index docs using alias in the newly created index
156+
indexDocs("test_user", "x-pack-test-password", "foo_alias", 2);
157+
refresh("foo_alias");
158+
159+
// wait so the ILM policy triggers rollover action, verify that the new index exists
160+
assertThat(awaitBusy(() -> {
161+
Request request = new Request("HEAD", "/" + "foo-logs-000002");
162+
int status;
163+
try {
164+
status = adminClient().performRequest(request).getStatusLine().getStatusCode();
165+
} catch (IOException e) {
166+
throw new RuntimeException(e);
167+
}
168+
return status == 200;
169+
}), is(true));
170+
171+
// test_user: index docs using alias, now should be able write to new index
172+
indexDocs("test_user", "x-pack-test-password", "foo_alias", 1);
173+
refresh("foo_alias");
174+
175+
// verify that the doc has been indexed into new write index
176+
awaitBusy(() -> {
177+
Request request = new Request("GET", "/foo-logs-000002/_search");
178+
Response response;
179+
try {
180+
response = adminClient().performRequest(request);
181+
try (InputStream content = response.getEntity().getContent()) {
182+
Map<String, Object> map = XContentHelper.convertToMap(JsonXContent.jsonXContent, content, false);
183+
return ((Integer) XContentMapValues.extractValue("hits.total", map)) == 1;
184+
}
185+
} catch (IOException e) {
186+
throw new RuntimeException(e);
187+
}
188+
});
189+
}
190+
191+
private void createNewSingletonPolicy(RestClient client, String policy, String phaseName, LifecycleAction action) throws IOException {
130192
Phase phase = new Phase(phaseName, TimeValue.ZERO, singletonMap(action.getWriteableName(), action));
131193
LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policy, singletonMap(phase.getName(), phase));
132194
XContentBuilder builder = jsonBuilder();
@@ -135,7 +197,7 @@ private void createNewSingletonPolicy(String policy, String phaseName, Lifecycle
135197
"{ \"policy\":" + Strings.toString(builder) + "}", ContentType.APPLICATION_JSON);
136198
Request request = new Request("PUT", "_ilm/policy/" + policy);
137199
request.setEntity(entity);
138-
client().performRequest(request);
200+
assertOK(client.performRequest(request));
139201
}
140202

141203
private void createIndexAsAdmin(String name, Settings settings, String mapping) throws IOException {
@@ -144,4 +206,59 @@ private void createIndexAsAdmin(String name, Settings settings, String mapping)
144206
+ ", \"mappings\" : {" + mapping + "} }");
145207
assertOK(adminClient().performRequest(request));
146208
}
209+
210+
private void createIndexAsAdmin(String name, String alias, boolean isWriteIndex) throws IOException {
211+
Request request = new Request("PUT", "/" + name);
212+
request.setJsonEntity("{ \"aliases\": { \""+alias+"\": {" + ((isWriteIndex) ? "\"is_write_index\" : true" : "")
213+
+ "} } }");
214+
assertOK(adminClient().performRequest(request));
215+
}
216+
217+
private void createIndexTemplate(String name, String pattern, String alias, String policy) throws IOException {
218+
Request request = new Request("PUT", "/_template/" + name);
219+
request.setJsonEntity("{\n" +
220+
" \"index_patterns\": [\""+pattern+"\"],\n" +
221+
" \"settings\": {\n" +
222+
" \"number_of_shards\": 1,\n" +
223+
" \"number_of_replicas\": 0,\n" +
224+
" \"index.lifecycle.name\": \""+policy+"\",\n" +
225+
" \"index.lifecycle.rollover_alias\": \""+alias+"\"\n" +
226+
" }\n" +
227+
" }");
228+
assertOK(adminClient().performRequest(request));
229+
}
230+
231+
private void createUser(String name, String password, String role) throws IOException {
232+
Request request = new Request("PUT", "/_security/user/" + name);
233+
request.setJsonEntity("{ \"password\": \""+password+"\", \"roles\": [ \""+ role+"\"] }");
234+
assertOK(adminClient().performRequest(request));
235+
}
236+
237+
private void createRole(String name, String alias) throws IOException {
238+
Request request = new Request("PUT", "/_security/role/" + name);
239+
request.setJsonEntity("{ \"indices\": [ { \"names\" : [ \""+ alias+"\"], \"privileges\": [ \"write\", \"manage\" ] } ] }");
240+
assertOK(adminClient().performRequest(request));
241+
}
242+
243+
private void indexDocs(String user, String passwd, String index, int noOfDocs) throws IOException {
244+
RestClientBuilder builder = RestClient.builder(adminClient().getNodes().toArray(new Node[0]));
245+
String token = basicAuthHeaderValue(user, new SecureString(passwd.toCharArray()));
246+
configureClient(builder, Settings.builder()
247+
.put(ThreadContext.PREFIX + ".Authorization", token)
248+
.build());
249+
builder.setStrictDeprecationMode(true);
250+
try (RestClient userClient = builder.build();) {
251+
252+
for (int cnt = 0; cnt < noOfDocs; cnt++) {
253+
Request request = new Request("POST", "/" + index + "/_doc");
254+
request.setJsonEntity(jsonDoc);
255+
assertOK(userClient.performRequest(request));
256+
}
257+
}
258+
}
259+
260+
private void refresh(String index) throws IOException {
261+
Request request = new Request("POST", "/" + index + "/_refresh");
262+
assertOK(adminClient().performRequest(request));
263+
}
147264
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,17 @@ static String getPutMappingIndexOrAlias(PutMappingRequest request, List<String>
246246
Optional<String> foundAlias = aliasMetaData.stream()
247247
.map(AliasMetaData::alias)
248248
.filter(authorizedIndicesList::contains)
249-
.filter(aliasName -> metaData.getAliasAndIndexLookup().get(aliasName).getIndices().size() == 1)
249+
.filter(aliasName -> {
250+
AliasOrIndex alias = metaData.getAliasAndIndexLookup().get(aliasName);
251+
List<IndexMetaData> indexMetadata = alias.getIndices();
252+
if (indexMetadata.size() == 1) {
253+
return true;
254+
} else {
255+
assert alias instanceof AliasOrIndex.Alias;
256+
IndexMetaData idxMeta = ((AliasOrIndex.Alias) alias).getWriteIndex();
257+
return idxMeta != null && idxMeta.getIndex().getName().equals(concreteIndexName);
258+
}
259+
})
250260
.findFirst();
251261
resolvedAliasOrIndex = foundAlias.orElse(concreteIndexName);
252262
} else {

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
import org.junit.Before;
7070

7171
import java.util.Arrays;
72+
import java.util.Collections;
7273
import java.util.HashMap;
7374
import java.util.HashSet;
7475
import java.util.List;
@@ -100,7 +101,6 @@ public class IndicesAndAliasesResolverTests extends ESTestCase {
100101
private IndicesAndAliasesResolver defaultIndicesResolver;
101102
private IndexNameExpressionResolver indexNameExpressionResolver;
102103
private Map<String, RoleDescriptor> roleMap;
103-
private FieldPermissionsCache fieldPermissionsCache;
104104

105105
@Before
106106
public void setup() {
@@ -134,13 +134,15 @@ public void setup() {
134134
.put(indexBuilder("-index11").settings(settings))
135135
.put(indexBuilder("-index20").settings(settings))
136136
.put(indexBuilder("-index21").settings(settings))
137+
.put(indexBuilder("logs-00001").putAlias(AliasMetaData.builder("logs-alias").writeIndex(false)).settings(settings))
138+
.put(indexBuilder("logs-00002").putAlias(AliasMetaData.builder("logs-alias").writeIndex(false)).settings(settings))
139+
.put(indexBuilder("logs-00003").putAlias(AliasMetaData.builder("logs-alias").writeIndex(true)).settings(settings))
137140
.put(indexBuilder(securityIndexName).settings(settings)).build();
138141

139142
if (withAlias) {
140143
metaData = SecurityTestUtils.addAliasToMetaData(metaData, securityIndexName);
141144
}
142145
this.metaData = metaData;
143-
this.fieldPermissionsCache = new FieldPermissionsCache(settings);
144146

145147
user = new User("user", "role");
146148
userDashIndices = new User("dash", "dash");
@@ -1344,6 +1346,29 @@ public void testDynamicPutMappingRequestFromAlias() {
13441346
request = new PutMappingRequest(Strings.EMPTY_ARRAY).setConcreteIndex(new Index(index, UUIDs.base64UUID()));
13451347
putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias(request, authorizedIndices, metaData);
13461348
assertEquals(index, putMappingIndexOrAlias);
1349+
1350+
}
1351+
1352+
public void testWhenAliasToMultipleIndicesAndUserIsAuthorizedUsingAliasReturnsAliasNameForDynamicPutMappingRequestOnWriteIndex() {
1353+
String index = "logs-00003"; // write index
1354+
PutMappingRequest request = new PutMappingRequest(Strings.EMPTY_ARRAY).setConcreteIndex(new Index(index, UUIDs.base64UUID()));
1355+
List<String> authorizedIndices = Collections.singletonList("logs-alias");
1356+
assert metaData.getAliasAndIndexLookup().get("logs-alias").getIndices().size() == 3;
1357+
String putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias(request, authorizedIndices, metaData);
1358+
String message = "user is authorized to access `logs-alias` and the put mapping request is for a write index"
1359+
+ "so this should have returned the alias name";
1360+
assertEquals(message, "logs-alias", putMappingIndexOrAlias);
1361+
}
1362+
1363+
public void testWhenAliasToMultipleIndicesAndUserIsAuthorizedUsingAliasReturnsIndexNameForDynamicPutMappingRequestOnReadIndex() {
1364+
String index = "logs-00002"; // read index
1365+
PutMappingRequest request = new PutMappingRequest(Strings.EMPTY_ARRAY).setConcreteIndex(new Index(index, UUIDs.base64UUID()));
1366+
List<String> authorizedIndices = Collections.singletonList("logs-alias");
1367+
assert metaData.getAliasAndIndexLookup().get("logs-alias").getIndices().size() == 3;
1368+
String putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias(request, authorizedIndices, metaData);
1369+
String message = "user is authorized to access `logs-alias` and the put mapping request is for a read index"
1370+
+ "so this should have returned the concrete index as fallback";
1371+
assertEquals(message, index, putMappingIndexOrAlias);
13471372
}
13481373

13491374
// TODO with the removal of DeleteByQuery is there another way to test resolving a write action?

0 commit comments

Comments
 (0)