Skip to content

Commit af8e407

Browse files
authored
[8.x] Failure Store Access Authorization (#123986) (#125298)
* Resolve * Fix more tests * Fix bwc test * Undo spacing diff * More whitespace stuff * Another * More
1 parent 137f35d commit af8e407

File tree

25 files changed

+3044
-328
lines changed

25 files changed

+3044
-328
lines changed

plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
package org.elasticsearch.example;
1111

1212
import org.elasticsearch.action.ActionListener;
13+
import org.elasticsearch.action.support.IndexComponentSelector;
1314
import org.elasticsearch.cluster.metadata.IndexAbstraction;
1415
import org.elasticsearch.cluster.metadata.Metadata;
1516
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse;
@@ -34,7 +35,6 @@
3435
import java.util.Arrays;
3536
import java.util.Collection;
3637
import java.util.Collections;
37-
import java.util.function.Supplier;
3838
import java.util.HashMap;
3939
import java.util.LinkedHashMap;
4040
import java.util.List;
@@ -117,19 +117,19 @@ public void loadAuthorizedIndices(
117117
) {
118118
if (isSuperuser(requestInfo.getAuthentication().getEffectiveSubject().getUser())) {
119119
listener.onResponse(new AuthorizedIndices() {
120-
public Supplier<Set<String>> all() {
120+
public Set<String> all(IndexComponentSelector selector) {
121121
return () -> indicesLookup.keySet();
122122
}
123-
public boolean check(String name) {
123+
public boolean check(String name, IndexComponentSelector selector) {
124124
return indicesLookup.containsKey(name);
125125
}
126126
});
127127
} else {
128128
listener.onResponse(new AuthorizedIndices() {
129-
public Supplier<Set<String>> all() {
129+
public Set<String> all(IndexComponentSelector selector) {
130130
return () -> Set.of();
131131
}
132-
public boolean check(String name) {
132+
public boolean check(String name, IndexComponentSelector selector) {
133133
return false;
134134
}
135135
});

server/src/main/java/org/elasticsearch/action/support/IndexComponentSelector.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,24 @@ public static IndexComponentSelector getByKey(String key) {
7272
return KEY_REGISTRY.get(key);
7373
}
7474

75+
/**
76+
* Like {@link #getByKey(String)} but throws an exception if the key is not recognised.
77+
* @return the selector if recognized. `null` input will return `DATA`.
78+
* @throws IllegalArgumentException if the key was not recognised.
79+
*/
80+
public static IndexComponentSelector getByKeyOrThrow(@Nullable String key) {
81+
if (key == null) {
82+
return DATA;
83+
}
84+
IndexComponentSelector selector = getByKey(key);
85+
if (selector == null) {
86+
throw new IllegalArgumentException(
87+
"Unknown key of index component selector [" + key + "], available options are: " + KEY_REGISTRY.keySet()
88+
);
89+
}
90+
return selector;
91+
}
92+
7593
public static IndexComponentSelector read(StreamInput in) throws IOException {
7694
byte id = in.readByte();
7795
if (in.getTransportVersion().onOrAfter(TransportVersions.REMOVE_ALL_APPLICABLE_SELECTOR_BACKPORT_8_19)

server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstraction.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ default boolean isDataStreamRelated() {
100100
return false;
101101
}
102102

103+
/**
104+
* @return whether this index abstraction is a failure index of a data stream
105+
*/
106+
default boolean isFailureIndexOfDataStream() {
107+
return false;
108+
}
109+
103110
/**
104111
* An index abstraction type.
105112
*/
@@ -183,6 +190,11 @@ public DataStream getParentDataStream() {
183190
return dataStream;
184191
}
185192

193+
@Override
194+
public boolean isFailureIndexOfDataStream() {
195+
return getParentDataStream() != null && getParentDataStream().isFailureStoreIndex(getName());
196+
}
197+
186198
@Override
187199
public boolean isHidden() {
188200
return isHidden;

server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
import java.util.HashSet;
2323
import java.util.List;
2424
import java.util.Set;
25-
import java.util.function.Predicate;
26-
import java.util.function.Supplier;
25+
import java.util.function.BiPredicate;
26+
import java.util.function.Function;
2727

2828
public class IndexAbstractionResolver {
2929

@@ -37,8 +37,8 @@ public List<String> resolveIndexAbstractions(
3737
Iterable<String> indices,
3838
IndicesOptions indicesOptions,
3939
Metadata metadata,
40-
Supplier<Set<String>> allAuthorizedAndAvailable,
41-
Predicate<String> isAuthorized,
40+
Function<IndexComponentSelector, Set<String>> allAuthorizedAndAvailableBySelector,
41+
BiPredicate<String, IndexComponentSelector> isAuthorized,
4242
boolean includeDataStreams
4343
) {
4444
List<String> finalIndices = new ArrayList<>();
@@ -64,14 +64,15 @@ public List<String> resolveIndexAbstractions(
6464
);
6565
}
6666
indexAbstraction = expressionAndSelector.v1();
67+
IndexComponentSelector selector = IndexComponentSelector.getByKeyOrThrow(selectorString);
6768

6869
// we always need to check for date math expressions
6970
indexAbstraction = IndexNameExpressionResolver.resolveDateMathExpression(indexAbstraction);
7071

7172
if (indicesOptions.expandWildcardExpressions() && Regex.isSimpleMatchPattern(indexAbstraction)) {
7273
wildcardSeen = true;
7374
Set<String> resolvedIndices = new HashSet<>();
74-
for (String authorizedIndex : allAuthorizedAndAvailable.get()) {
75+
for (String authorizedIndex : allAuthorizedAndAvailableBySelector.apply(selector)) {
7576
if (Regex.simpleMatch(indexAbstraction, authorizedIndex)
7677
&& isIndexVisible(
7778
indexAbstraction,
@@ -102,7 +103,7 @@ && isIndexVisible(
102103
resolveSelectorsAndCollect(indexAbstraction, selectorString, indicesOptions, resolvedIndices, metadata);
103104
if (minus) {
104105
finalIndices.removeAll(resolvedIndices);
105-
} else if (indicesOptions.ignoreUnavailable() == false || isAuthorized.test(indexAbstraction)) {
106+
} else if (indicesOptions.ignoreUnavailable() == false || isAuthorized.test(indexAbstraction, selector)) {
106107
// Unauthorized names are considered unavailable, so if `ignoreUnavailable` is `true` they should be silently
107108
// discarded from the `finalIndices` list. Other "ways of unavailable" must be handled by the action
108109
// handler, see: https://github.com/elastic/elasticsearch/issues/90215

server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.elasticsearch.common.util.CollectionUtils;
2828
import org.elasticsearch.common.util.concurrent.ThreadContext;
2929
import org.elasticsearch.common.util.set.Sets;
30+
import org.elasticsearch.core.Assertions;
3031
import org.elasticsearch.core.Nullable;
3132
import org.elasticsearch.core.Predicates;
3233
import org.elasticsearch.core.Tuple;
@@ -831,6 +832,14 @@ public static boolean hasSelectorSuffix(String expression) {
831832
return expression.contains(SelectorResolver.SELECTOR_SEPARATOR);
832833
}
833834

835+
public static boolean hasSelector(@Nullable String expression, IndexComponentSelector selector) {
836+
Objects.requireNonNull(selector, "null selectors not supported");
837+
if (expression == null) {
838+
return false;
839+
}
840+
return expression.endsWith(SelectorResolver.SELECTOR_SEPARATOR + selector.getKey());
841+
}
842+
834843
/**
835844
* @return If the specified string is a selector expression then this method returns the base expression and its selector part.
836845
*/
@@ -852,6 +861,14 @@ public static String combineSelectorExpression(String baseExpression, @Nullable
852861
: (baseExpression + SelectorResolver.SELECTOR_SEPARATOR + selectorExpression);
853862
}
854863

864+
public static void assertExpressionHasNullOrDataSelector(String expression) {
865+
if (Assertions.ENABLED) {
866+
var tuple = splitSelectorExpression(expression);
867+
assert tuple.v2() == null || IndexComponentSelector.DATA.getKey().equals(tuple.v2())
868+
: "Expected expression [" + expression + "] to have a data selector but found [" + tuple.v2() + "]";
869+
}
870+
}
871+
855872
/**
856873
* Resolve an array of expressions to the set of indices and aliases that these expressions match.
857874
*/

server/src/test/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolverTests.java

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import java.util.List;
2727
import java.util.Set;
2828
import java.util.concurrent.TimeUnit;
29-
import java.util.function.Supplier;
3029

3130
import static org.elasticsearch.index.mapper.MapperService.SINGLE_MAPPING_NAME;
3231
import static org.elasticsearch.indices.SystemIndices.EXTERNAL_SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY;
@@ -47,7 +46,7 @@ public class IndexAbstractionResolverTests extends ESTestCase {
4746
private String dateTimeIndexTomorrow;
4847

4948
// Only used when resolving wildcard expressions
50-
private final Supplier<Set<String>> defaultMask = () -> Set.of("index1", "index2", "data-stream1");
49+
private final Set<String> defaultMask = Set.of("index1", "index2", "data-stream1");
5150

5251
@Override
5352
public void setUp() throws Exception {
@@ -208,13 +207,11 @@ public void testResolveIndexAbstractions() {
208207

209208
public void testIsIndexVisible() {
210209
assertThat(isIndexVisible("index1", null), is(true));
211-
assertThat(isIndexVisible("index1", "*"), is(true));
212210
assertThat(isIndexVisible("index1", "data"), is(true));
213211
assertThat(isIndexVisible("index1", "failures"), is(false)); // *
214212
// * Indices don't have failure components so the failure component is not visible
215213

216214
assertThat(isIndexVisible("data-stream1", null), is(true));
217-
assertThat(isIndexVisible("data-stream1", "*"), is(true));
218215
assertThat(isIndexVisible("data-stream1", "data"), is(true));
219216
assertThat(isIndexVisible("data-stream1", "failures"), is(true));
220217
}
@@ -281,14 +278,14 @@ public void testIsNetNewSystemIndexVisible() {
281278
indexAbstractionResolver = new IndexAbstractionResolver(indexNameExpressionResolver);
282279

283280
// this covers the GET * case -- with system access, you can see everything
284-
assertThat(isIndexVisible("other", "*"), is(true));
285-
assertThat(isIndexVisible(".foo", "*"), is(true));
286-
assertThat(isIndexVisible(".bar", "*"), is(true));
281+
assertThat(isIndexVisible("other", null), is(true));
282+
assertThat(isIndexVisible(".foo", null), is(true));
283+
assertThat(isIndexVisible(".bar", null), is(true));
287284

288285
// but if you don't ask for hidden and aliases, you won't see hidden indices or aliases, naturally
289-
assertThat(isIndexVisible("other", "*", noHiddenNoAliases), is(true));
290-
assertThat(isIndexVisible(".foo", "*", noHiddenNoAliases), is(false));
291-
assertThat(isIndexVisible(".bar", "*", noHiddenNoAliases), is(false));
286+
assertThat(isIndexVisible("other", null, noHiddenNoAliases), is(true));
287+
assertThat(isIndexVisible(".foo", null, noHiddenNoAliases), is(false));
288+
assertThat(isIndexVisible(".bar", null, noHiddenNoAliases), is(false));
292289
}
293290

294291
{
@@ -298,14 +295,14 @@ public void testIsNetNewSystemIndexVisible() {
298295
indexAbstractionResolver = new IndexAbstractionResolver(indexNameExpressionResolver);
299296

300297
// this covers the GET * case -- without system access, you can't see everything
301-
assertThat(isIndexVisible("other", "*"), is(true));
302-
assertThat(isIndexVisible(".foo", "*"), is(false));
303-
assertThat(isIndexVisible(".bar", "*"), is(false));
298+
assertThat(isIndexVisible("other", null), is(true));
299+
assertThat(isIndexVisible(".foo", null), is(false));
300+
assertThat(isIndexVisible(".bar", null), is(false));
304301

305302
// no difference here in the datastream case, you can't see these then, either
306-
assertThat(isIndexVisible("other", "*", noHiddenNoAliases), is(true));
307-
assertThat(isIndexVisible(".foo", "*", noHiddenNoAliases), is(false));
308-
assertThat(isIndexVisible(".bar", "*", noHiddenNoAliases), is(false));
303+
assertThat(isIndexVisible("other", null, noHiddenNoAliases), is(true));
304+
assertThat(isIndexVisible(".foo", null, noHiddenNoAliases), is(false));
305+
assertThat(isIndexVisible(".bar", null, noHiddenNoAliases), is(false));
309306
}
310307

311308
{
@@ -316,14 +313,14 @@ public void testIsNetNewSystemIndexVisible() {
316313
indexAbstractionResolver = new IndexAbstractionResolver(indexNameExpressionResolver);
317314

318315
// this covers the GET * case -- with product (only) access, you can't see everything
319-
assertThat(isIndexVisible("other", "*"), is(true));
320-
assertThat(isIndexVisible(".foo", "*"), is(false));
321-
assertThat(isIndexVisible(".bar", "*"), is(false));
316+
assertThat(isIndexVisible("other", null), is(true));
317+
assertThat(isIndexVisible(".foo", null), is(false));
318+
assertThat(isIndexVisible(".bar", null), is(false));
322319

323320
// no difference here in the datastream case, you can't see these then, either
324-
assertThat(isIndexVisible("other", "*", noHiddenNoAliases), is(true));
325-
assertThat(isIndexVisible(".foo", "*", noHiddenNoAliases), is(false));
326-
assertThat(isIndexVisible(".bar", "*", noHiddenNoAliases), is(false));
321+
assertThat(isIndexVisible("other", null, noHiddenNoAliases), is(true));
322+
assertThat(isIndexVisible(".foo", null, noHiddenNoAliases), is(false));
323+
assertThat(isIndexVisible(".bar", null, noHiddenNoAliases), is(false));
327324
}
328325
}
329326

@@ -349,8 +346,15 @@ private List<String> resolveAbstractionsSelectorAllowed(List<String> expressions
349346
return resolveAbstractions(expressions, IndicesOptions.strictExpandOpen(), defaultMask);
350347
}
351348

352-
private List<String> resolveAbstractions(List<String> expressions, IndicesOptions indicesOptions, Supplier<Set<String>> mask) {
353-
return indexAbstractionResolver.resolveIndexAbstractions(expressions, indicesOptions, metadata, mask, (idx) -> true, true);
349+
private List<String> resolveAbstractions(List<String> expressions, IndicesOptions indicesOptions, Set<String> mask) {
350+
return indexAbstractionResolver.resolveIndexAbstractions(
351+
expressions,
352+
indicesOptions,
353+
metadata,
354+
(ignored) -> mask,
355+
(ignored, nothing) -> true,
356+
true
357+
);
354358
}
355359

356360
private boolean isIndexVisible(String index, String selector) {

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

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
import org.elasticsearch.action.ActionListener;
1212
import org.elasticsearch.action.ActionRequestValidationException;
1313
import org.elasticsearch.action.IndicesRequest;
14+
import org.elasticsearch.action.support.IndexComponentSelector;
15+
import org.elasticsearch.cluster.metadata.DataStream;
1416
import org.elasticsearch.cluster.metadata.IndexAbstraction;
17+
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
1518
import org.elasticsearch.cluster.metadata.Metadata;
1619
import org.elasticsearch.common.bytes.BytesReference;
1720
import org.elasticsearch.common.io.stream.BytesStreamOutput;
@@ -38,7 +41,6 @@
3841
import java.util.Map;
3942
import java.util.Objects;
4043
import java.util.Set;
41-
import java.util.function.Supplier;
4244
import java.util.stream.Collectors;
4345

4446
import static org.elasticsearch.action.ValidateActions.addValidationError;
@@ -280,22 +282,23 @@ default AuthorizationInfo getAuthenticatedUserAuthorizationInfo() {
280282
}
281283

282284
/**
283-
* Used to retrieve index-like resources that the user has access to, for a specific access action type,
285+
* Used to retrieve index-like resources that the user has access to, for a specific access action type and selector,
284286
* at a specific point in time (for a fixed cluster state view).
285287
* It can also be used to check if a specific resource name is authorized (access to the resource name
286288
* can be authorized even if it doesn't exist).
287289
*/
288290
interface AuthorizedIndices {
289291
/**
290-
* Returns all the index-like resource names that are available and accessible for an action type by a user,
292+
* Returns all the index-like resource names that are available and accessible for an action type and selector by a user,
291293
* at a fixed point in time (for a single cluster state view).
294+
* The result is cached and subsequent calls to this method are idempotent.
292295
*/
293-
Supplier<Set<String>> all();
296+
Set<String> all(IndexComponentSelector selector);
294297

295298
/**
296299
* Checks if an index-like resource name is authorized, for an action by a user. The resource might or might not exist.
297300
*/
298-
boolean check(String name);
301+
boolean check(String name, IndexComponentSelector selector);
299302
}
300303

301304
/**
@@ -365,6 +368,31 @@ public ActionRequestValidationException validate(ActionRequestValidationExceptio
365368
&& application.length == 0) {
366369
validationException = addValidationError("must specify at least one privilege", validationException);
367370
}
371+
if (index != null) {
372+
// no need to validate failure-store related constraints if it's not enabled
373+
if (DataStream.isFailureStoreFeatureFlagEnabled()) {
374+
for (RoleDescriptor.IndicesPrivileges indexPrivilege : index) {
375+
if (indexPrivilege.getIndices() != null
376+
&& Arrays.stream(indexPrivilege.getIndices())
377+
// best effort prevent users from attempting to check failure selectors
378+
.anyMatch(idx -> IndexNameExpressionResolver.hasSelector(idx, IndexComponentSelector.FAILURES))) {
379+
validationException = addValidationError(
380+
// TODO adjust message once HasPrivileges check supports checking failure store privileges
381+
"failures selector is not supported in index patterns",
382+
validationException
383+
);
384+
}
385+
if (indexPrivilege.getPrivileges() != null
386+
&& Arrays.stream(indexPrivilege.getPrivileges())
387+
.anyMatch(p -> "read_failure_store".equals(p) || "manage_failure_store".equals(p))) {
388+
validationException = addValidationError(
389+
"checking failure store privileges is not supported",
390+
validationException
391+
);
392+
}
393+
}
394+
}
395+
}
368396
return validationException;
369397
}
370398

0 commit comments

Comments
 (0)