Skip to content

Commit ebf7920

Browse files
authored
put mapping authorization for alias with write-index and multiple read indices (#40834)
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 da37265 commit ebf7920

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

+120-3
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.value", 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

+11-1
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,17 @@ static String getPutMappingIndexOrAlias(PutMappingRequest request, List<String>
249249
Optional<String> foundAlias = aliasMetaData.stream()
250250
.map(AliasMetaData::alias)
251251
.filter(authorizedIndicesList::contains)
252-
.filter(aliasName -> metaData.getAliasAndIndexLookup().get(aliasName).getIndices().size() == 1)
252+
.filter(aliasName -> {
253+
AliasOrIndex alias = metaData.getAliasAndIndexLookup().get(aliasName);
254+
List<IndexMetaData> indexMetadata = alias.getIndices();
255+
if (indexMetadata.size() == 1) {
256+
return true;
257+
} else {
258+
assert alias instanceof AliasOrIndex.Alias;
259+
IndexMetaData idxMeta = ((AliasOrIndex.Alias) alias).getWriteIndex();
260+
return idxMeta != null && idxMeta.getIndex().getName().equals(concreteIndexName);
261+
}
262+
})
253263
.findFirst();
254264
resolvedAliasOrIndex = foundAlias.orElse(concreteIndexName);
255265
} else {

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

+27-2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
import org.junit.Before;
7373

7474
import java.util.Arrays;
75+
import java.util.Collections;
7576
import java.util.HashMap;
7677
import java.util.HashSet;
7778
import java.util.List;
@@ -104,7 +105,6 @@ public class IndicesAndAliasesResolverTests extends ESTestCase {
104105
private IndicesAndAliasesResolver defaultIndicesResolver;
105106
private IndexNameExpressionResolver indexNameExpressionResolver;
106107
private Map<String, RoleDescriptor> roleMap;
107-
private FieldPermissionsCache fieldPermissionsCache;
108108

109109
@Before
110110
public void setup() {
@@ -138,13 +138,15 @@ public void setup() {
138138
.put(indexBuilder("-index11").settings(settings))
139139
.put(indexBuilder("-index20").settings(settings))
140140
.put(indexBuilder("-index21").settings(settings))
141+
.put(indexBuilder("logs-00001").putAlias(AliasMetaData.builder("logs-alias").writeIndex(false)).settings(settings))
142+
.put(indexBuilder("logs-00002").putAlias(AliasMetaData.builder("logs-alias").writeIndex(false)).settings(settings))
143+
.put(indexBuilder("logs-00003").putAlias(AliasMetaData.builder("logs-alias").writeIndex(true)).settings(settings))
141144
.put(indexBuilder(securityIndexName).settings(settings)).build();
142145

143146
if (withAlias) {
144147
metaData = SecurityTestUtils.addAliasToMetaData(metaData, securityIndexName);
145148
}
146149
this.metaData = metaData;
147-
this.fieldPermissionsCache = new FieldPermissionsCache(settings);
148150

149151
user = new User("user", "role");
150152
userDashIndices = new User("dash", "dash");
@@ -1355,6 +1357,29 @@ public void testDynamicPutMappingRequestFromAlias() {
13551357
request = new PutMappingRequest(Strings.EMPTY_ARRAY).setConcreteIndex(new Index(index, UUIDs.base64UUID()));
13561358
putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias(request, authorizedIndices, metaData);
13571359
assertEquals(index, putMappingIndexOrAlias);
1360+
1361+
}
1362+
1363+
public void testWhenAliasToMultipleIndicesAndUserIsAuthorizedUsingAliasReturnsAliasNameForDynamicPutMappingRequestOnWriteIndex() {
1364+
String index = "logs-00003"; // write 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 write index"
1370+
+ "so this should have returned the alias name";
1371+
assertEquals(message, "logs-alias", putMappingIndexOrAlias);
1372+
}
1373+
1374+
public void testWhenAliasToMultipleIndicesAndUserIsAuthorizedUsingAliasReturnsIndexNameForDynamicPutMappingRequestOnReadIndex() {
1375+
String index = "logs-00002"; // read index
1376+
PutMappingRequest request = new PutMappingRequest(Strings.EMPTY_ARRAY).setConcreteIndex(new Index(index, UUIDs.base64UUID()));
1377+
List<String> authorizedIndices = Collections.singletonList("logs-alias");
1378+
assert metaData.getAliasAndIndexLookup().get("logs-alias").getIndices().size() == 3;
1379+
String putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias(request, authorizedIndices, metaData);
1380+
String message = "user is authorized to access `logs-alias` and the put mapping request is for a read index"
1381+
+ "so this should have returned the concrete index as fallback";
1382+
assertEquals(message, index, putMappingIndexOrAlias);
13581383
}
13591384

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

0 commit comments

Comments
 (0)