Skip to content

Commit c58ac45

Browse files
authored
Failure Store Access Authorization (#123986)
This PR implements authorization logic for failure store access. It builds on #122715. Access to the failure store is granted by two privileges: `read_failure_store` and `manage_failure_store`. Either of these privileges lets a user access a failure store via the `::failures` selector, as well as access its backing failure indices. `read_failure_store` grants read access (for example to search documents in a failure store), `manage_failure_store` grants access to write operations, such as rollover. Users with only `read` or `manage` on a data stream do not get failure store access. Vice versa, users with `read_failure_store` and `manage_failure_store` do not get access to regular data in a data stream. The PR implements this by making authorization logic selector-aware. It involves two main changes: 1. Index permission groups now compare the selector under which an index resource is accessed to the selector associated with the group. 2. The `AuthorizedIndices` interface likewise uses selectors to decide which indices to treat as authorized. This part of the change requires a sizable refactor and changes to the interface. The high-level behavior for selector-aware search is as follows: For a user with `read_failure_store` over data stream `logs`: - `POST /logs::failures/_search` returns the documents in the failure store. - `POST /logs/_search` returns a 403. - `POST /logs/_search?ignore_unavailable=true` and `POST /*/_search` return an empty result. Similarly, for a user with `read` over data stream `logs`: - `POST /logs::failures/_search` returns a 403. - `POST /logs/_search` returns documents in the data stream. - `POST /logs::failures/_search?ignore_unavailable=true` and `POST /*::failures/_search` return an empty result. A user with both `read` and `read_failure_store` over data stream `logs` gets access to both `POST /logs::failures/_search` and `POST /logs/_search`. The index privilege `all` automatically grants access to both data and the failures store, as well as all hypothetical future selectors. Resolves: ES-10873
1 parent 54240d3 commit c58ac45

File tree

24 files changed

+3046
-328
lines changed

24 files changed

+3046
-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.action.support.SubscribableListener;
1415
import org.elasticsearch.cluster.metadata.IndexAbstraction;
1516
import org.elasticsearch.cluster.metadata.ProjectMetadata;
@@ -35,7 +36,6 @@
3536
import java.util.Arrays;
3637
import java.util.Collection;
3738
import java.util.Collections;
38-
import java.util.function.Supplier;
3939
import java.util.HashMap;
4040
import java.util.LinkedHashMap;
4141
import java.util.List;
@@ -119,19 +119,19 @@ public void loadAuthorizedIndices(
119119
) {
120120
if (isSuperuser(requestInfo.getAuthentication().getEffectiveSubject().getUser())) {
121121
listener.onResponse(new AuthorizedIndices() {
122-
public Supplier<Set<String>> all() {
122+
public Set<String> all(IndexComponentSelector selector) {
123123
return () -> indicesLookup.keySet();
124124
}
125-
public boolean check(String name) {
125+
public boolean check(String name, IndexComponentSelector selector) {
126126
return indicesLookup.containsKey(name);
127127
}
128128
});
129129
} else {
130130
listener.onResponse(new AuthorizedIndices() {
131-
public Supplier<Set<String>> all() {
131+
public Set<String> all(IndexComponentSelector selector) {
132132
return () -> Set.of();
133133
}
134-
public boolean check(String name) {
134+
public boolean check(String name, IndexComponentSelector selector) {
135135
return false;
136136
}
137137
});

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)

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
ProjectMetadata projectMetadata,
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, projectMetadata);
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
@@ -28,6 +28,7 @@
2828
import org.elasticsearch.common.util.CollectionUtils;
2929
import org.elasticsearch.common.util.concurrent.ThreadContext;
3030
import org.elasticsearch.common.util.set.Sets;
31+
import org.elasticsearch.core.Assertions;
3132
import org.elasticsearch.core.Nullable;
3233
import org.elasticsearch.core.Predicates;
3334
import org.elasticsearch.core.Tuple;
@@ -1001,6 +1002,14 @@ public static boolean hasSelectorSuffix(String expression) {
10011002
return expression.contains(SelectorResolver.SELECTOR_SEPARATOR);
10021003
}
10031004

1005+
public static boolean hasSelector(@Nullable String expression, IndexComponentSelector selector) {
1006+
Objects.requireNonNull(selector, "null selectors not supported");
1007+
if (expression == null) {
1008+
return false;
1009+
}
1010+
return expression.endsWith(SelectorResolver.SELECTOR_SEPARATOR + selector.getKey());
1011+
}
1012+
10041013
/**
10051014
* @return If the specified string is a selector expression then this method returns the base expression and its selector part.
10061015
*/
@@ -1022,6 +1031,14 @@ public static String combineSelectorExpression(String baseExpression, @Nullable
10221031
: (baseExpression + SelectorResolver.SELECTOR_SEPARATOR + selectorExpression);
10231032
}
10241033

1034+
public static void assertExpressionHasNullOrDataSelector(String expression) {
1035+
if (Assertions.ENABLED) {
1036+
var tuple = splitSelectorExpression(expression);
1037+
assert tuple.v2() == null || IndexComponentSelector.DATA.getKey().equals(tuple.v2())
1038+
: "Expected expression [" + expression + "] to have a data selector but found [" + tuple.v2() + "]";
1039+
}
1040+
}
1041+
10251042
/**
10261043
* Resolve an array of expressions to the set of indices and aliases that these expressions match.
10271044
*/

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

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

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

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

5352
@Override
5453
public void setUp() throws Exception {
@@ -215,13 +214,11 @@ public void testResolveIndexAbstractions() {
215214

216215
public void testIsIndexVisible() {
217216
assertThat(isIndexVisible("index1", null), is(true));
218-
assertThat(isIndexVisible("index1", "*"), is(true));
219217
assertThat(isIndexVisible("index1", "data"), is(true));
220218
assertThat(isIndexVisible("index1", "failures"), is(false)); // *
221219
// * Indices don't have failure components so the failure component is not visible
222220

223221
assertThat(isIndexVisible("data-stream1", null), is(true));
224-
assertThat(isIndexVisible("data-stream1", "*"), is(true));
225222
assertThat(isIndexVisible("data-stream1", "data"), is(true));
226223
assertThat(isIndexVisible("data-stream1", "failures"), is(true));
227224
}
@@ -290,14 +287,14 @@ public void testIsNetNewSystemIndexVisible() {
290287
indexAbstractionResolver = new IndexAbstractionResolver(indexNameExpressionResolver);
291288

292289
// this covers the GET * case -- with system access, you can see everything
293-
assertThat(isIndexVisible("other", "*"), is(true));
294-
assertThat(isIndexVisible(".foo", "*"), is(true));
295-
assertThat(isIndexVisible(".bar", "*"), is(true));
290+
assertThat(isIndexVisible("other", null), is(true));
291+
assertThat(isIndexVisible(".foo", null), is(true));
292+
assertThat(isIndexVisible(".bar", null), is(true));
296293

297294
// but if you don't ask for hidden and aliases, you won't see hidden indices or aliases, naturally
298-
assertThat(isIndexVisible("other", "*", noHiddenNoAliases), is(true));
299-
assertThat(isIndexVisible(".foo", "*", noHiddenNoAliases), is(false));
300-
assertThat(isIndexVisible(".bar", "*", noHiddenNoAliases), is(false));
295+
assertThat(isIndexVisible("other", null, noHiddenNoAliases), is(true));
296+
assertThat(isIndexVisible(".foo", null, noHiddenNoAliases), is(false));
297+
assertThat(isIndexVisible(".bar", null, noHiddenNoAliases), is(false));
301298
}
302299

303300
{
@@ -311,14 +308,14 @@ public void testIsNetNewSystemIndexVisible() {
311308
indexAbstractionResolver = new IndexAbstractionResolver(indexNameExpressionResolver);
312309

313310
// this covers the GET * case -- without system access, you can't see everything
314-
assertThat(isIndexVisible("other", "*"), is(true));
315-
assertThat(isIndexVisible(".foo", "*"), is(false));
316-
assertThat(isIndexVisible(".bar", "*"), is(false));
311+
assertThat(isIndexVisible("other", null), is(true));
312+
assertThat(isIndexVisible(".foo", null), is(false));
313+
assertThat(isIndexVisible(".bar", null), is(false));
317314

318315
// no difference here in the datastream case, you can't see these then, either
319-
assertThat(isIndexVisible("other", "*", noHiddenNoAliases), is(true));
320-
assertThat(isIndexVisible(".foo", "*", noHiddenNoAliases), is(false));
321-
assertThat(isIndexVisible(".bar", "*", noHiddenNoAliases), is(false));
316+
assertThat(isIndexVisible("other", null, noHiddenNoAliases), is(true));
317+
assertThat(isIndexVisible(".foo", null, noHiddenNoAliases), is(false));
318+
assertThat(isIndexVisible(".bar", null, noHiddenNoAliases), is(false));
322319
}
323320

324321
{
@@ -333,14 +330,14 @@ public void testIsNetNewSystemIndexVisible() {
333330
indexAbstractionResolver = new IndexAbstractionResolver(indexNameExpressionResolver);
334331

335332
// this covers the GET * case -- with product (only) access, you can't see everything
336-
assertThat(isIndexVisible("other", "*"), is(true));
337-
assertThat(isIndexVisible(".foo", "*"), is(false));
338-
assertThat(isIndexVisible(".bar", "*"), is(false));
333+
assertThat(isIndexVisible("other", null), is(true));
334+
assertThat(isIndexVisible(".foo", null), is(false));
335+
assertThat(isIndexVisible(".bar", null), is(false));
339336

340337
// no difference here in the datastream case, you can't see these then, either
341-
assertThat(isIndexVisible("other", "*", noHiddenNoAliases), is(true));
342-
assertThat(isIndexVisible(".foo", "*", noHiddenNoAliases), is(false));
343-
assertThat(isIndexVisible(".bar", "*", noHiddenNoAliases), is(false));
338+
assertThat(isIndexVisible("other", null, noHiddenNoAliases), is(true));
339+
assertThat(isIndexVisible(".foo", null, noHiddenNoAliases), is(false));
340+
assertThat(isIndexVisible(".bar", null, noHiddenNoAliases), is(false));
344341
}
345342
}
346343

@@ -366,8 +363,15 @@ private List<String> resolveAbstractionsSelectorAllowed(List<String> expressions
366363
return resolveAbstractions(expressions, IndicesOptions.strictExpandOpen(), defaultMask);
367364
}
368365

369-
private List<String> resolveAbstractions(List<String> expressions, IndicesOptions indicesOptions, Supplier<Set<String>> mask) {
370-
return indexAbstractionResolver.resolveIndexAbstractions(expressions, indicesOptions, projectMetadata, mask, (idx) -> true, true);
366+
private List<String> resolveAbstractions(List<String> expressions, IndicesOptions indicesOptions, Set<String> mask) {
367+
return indexAbstractionResolver.resolveIndexAbstractions(
368+
expressions,
369+
indicesOptions,
370+
projectMetadata,
371+
(ignored) -> mask,
372+
(ignored, nothing) -> true,
373+
true
374+
);
371375
}
372376

373377
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,8 +11,11 @@
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;
1415
import org.elasticsearch.action.support.SubscribableListener;
16+
import org.elasticsearch.cluster.metadata.DataStream;
1517
import org.elasticsearch.cluster.metadata.IndexAbstraction;
18+
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
1619
import org.elasticsearch.cluster.metadata.ProjectMetadata;
1720
import org.elasticsearch.common.bytes.BytesReference;
1821
import org.elasticsearch.common.io.stream.BytesStreamOutput;
@@ -39,7 +42,6 @@
3942
import java.util.Map;
4043
import java.util.Objects;
4144
import java.util.Set;
42-
import java.util.function.Supplier;
4345
import java.util.stream.Collectors;
4446

4547
import static org.elasticsearch.action.ValidateActions.addValidationError;
@@ -281,22 +283,23 @@ default AuthorizationInfo getAuthenticatedUserAuthorizationInfo() {
281283
}
282284

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

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

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

0 commit comments

Comments
 (0)