Skip to content

Commit bbeb05a

Browse files
committed
Update View CRUD Actions to be Index Actions
1 parent 48060a0 commit bbeb05a

File tree

20 files changed

+471
-55
lines changed

20 files changed

+471
-55
lines changed

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,19 +97,21 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
9797
* @param resolveAliases, aliases will be included in the result, if false we treat them like they do not exist
9898
* @param allowEmptyExpressions, when an expression does not result in any indices, if false it throws an error if true it treats it as
9999
* an empty result
100+
* @param resolveViews, views will be included in the result, if false we treat them like they do not exist
100101
*/
101102
public record WildcardOptions(
102103
boolean matchOpen,
103104
boolean matchClosed,
104105
boolean includeHidden,
105106
boolean resolveAliases,
106-
boolean allowEmptyExpressions
107+
boolean allowEmptyExpressions,
108+
boolean resolveViews
107109
) implements ToXContentFragment {
108110

109111
public static final String EXPAND_WILDCARDS = "expand_wildcards";
110112
public static final String ALLOW_NO_INDICES = "allow_no_indices";
111113

112-
public static final WildcardOptions DEFAULT = new WildcardOptions(true, false, false, true, true);
114+
public static final WildcardOptions DEFAULT = new WildcardOptions(true, false, false, true, true, false);
113115

114116
public static WildcardOptions parseParameters(Object expandWildcards, Object allowNoIndices, WildcardOptions defaultOptions) {
115117
if (expandWildcards == null && allowNoIndices == null) {
@@ -178,6 +180,7 @@ public static class Builder {
178180
private boolean includeHidden;
179181
private boolean resolveAliases;
180182
private boolean allowEmptyExpressions;
183+
private boolean resolveViews;
181184

182185
Builder() {
183186
this(DEFAULT);
@@ -189,6 +192,7 @@ public static class Builder {
189192
includeHidden = options.includeHidden;
190193
resolveAliases = options.resolveAliases;
191194
allowEmptyExpressions = options.allowEmptyExpressions;
195+
resolveViews = options.resolveViews;
192196
}
193197

194198
/**
@@ -244,6 +248,14 @@ public Builder matchNone() {
244248
return this;
245249
}
246250

251+
/**
252+
* Resolve views. Defaults to false
253+
*/
254+
public Builder resolveViews(boolean resolveViews) {
255+
this.resolveViews = resolveViews;
256+
return this;
257+
}
258+
247259
/**
248260
* Maximises the resolution of indices, we will match open, closed and hidden targets.
249261
*/
@@ -281,7 +293,7 @@ public Builder expandStates(String[] expandStates) {
281293
}
282294

283295
public WildcardOptions build() {
284-
return new WildcardOptions(matchOpen, matchClosed, includeHidden, resolveAliases, allowEmptyExpressions);
296+
return new WildcardOptions(matchOpen, matchClosed, includeHidden, resolveAliases, allowEmptyExpressions, resolveViews);
285297
}
286298
}
287299

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,7 @@ private static boolean isIndexVisible(
308308
throw new IllegalStateException("could not resolve index abstraction [" + index + "]");
309309
}
310310
if (indexAbstraction.getType() == IndexAbstraction.Type.VIEW) {
311-
// TODO: perhaps revisit this in the future if we make views "visible" or "hidden"?
312-
return false;
311+
return indicesOptions.wildcardOptions().resolveViews();
313312
}
314313
final boolean isHidden = indexAbstraction.isHidden();
315314
boolean isVisible = isWildcardExpression == false

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,10 @@ public static boolean shouldIncludeFailureIndices(IndicesOptions indicesOptions,
725725
return indicesOptions.includeFailureIndices();
726726
}
727727

728+
public static boolean shouldPreserveViews(IndicesOptions indicesOptions) {
729+
return indicesOptions.wildcardOptions().resolveViews();
730+
}
731+
728732
private static boolean resolvesToMoreThanOneIndex(IndexAbstraction indexAbstraction, Context context, ResolvedExpression expression) {
729733
if (indexAbstraction.getType() == Type.ALIAS && indexAbstraction.isDataStreamRelated()) {
730734
// We inline this logic instead of calling aliasDataStreams because we want to return as soon as we've identified that we have
@@ -1788,7 +1792,7 @@ private static boolean shouldExpandToIndexAbstraction(
17881792
String wildcardExpression,
17891793
IndexAbstraction indexAbstraction
17901794
) {
1791-
if (indexAbstraction.getType() == Type.VIEW) {
1795+
if (shouldPreserveViews(context.getOptions()) == false && indexAbstraction.getType() == Type.VIEW) {
17921796
return false;
17931797
}
17941798
if (context.getOptions().ignoreAliases() && indexAbstraction.getType() == Type.ALIAS) {
@@ -1843,8 +1847,9 @@ private static Set<ResolvedExpression> expandToOpenClosed(
18431847
} else if (context.isPreserveDataStreams() && indexAbstraction.getType() == Type.DATA_STREAM) {
18441848
resources.add(new ResolvedExpression(indexAbstraction.getName(), selector));
18451849
} else if (indexAbstraction.getType() == Type.VIEW) {
1846-
// a view cannot expand to any indices, return an empty set
1847-
return Set.of();
1850+
if (shouldPreserveViews(context.getOptions())) {
1851+
resources.add(new ResolvedExpression(indexAbstraction.getName(), selector));
1852+
}
18481853
} else {
18491854
if (shouldIncludeRegularIndices(context.getOptions(), selector)) {
18501855
for (int i = 0, n = indexAbstraction.getIndices().size(); i < n; i++) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1851,7 +1851,7 @@ static SortedMap<String, IndexAbstraction> buildIndicesLookup(
18511851
ViewMetadata viewMetadata,
18521852
ImmutableOpenMap<String, IndexMetadata> indices
18531853
) {
1854-
if (indices.isEmpty()) {
1854+
if (indices.isEmpty() && viewMetadata.views().isEmpty()) {
18551855
return Collections.emptySortedMap();
18561856
}
18571857
Map<String, IndexAbstraction> indicesLookup = new HashMap<>();

server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/create/CreateSnapshotRequestTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ public void testToXContent() throws IOException {
8686
randomBoolean(),
8787
randomBoolean(),
8888
defaultResolveAliasForThisRequest,
89-
randomBoolean()
89+
randomBoolean(),
90+
false // Specifying views in the create snapshot request is not supported
9091
)
9192
)
9293
.gatekeeperOptions(IndicesOptions.GatekeeperOptions.builder().allowSelectors(false).includeFailureIndices(true).build())

server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequestTests.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@
3030
import java.util.Map;
3131

3232
import static org.hamcrest.Matchers.containsString;
33-
import static org.junit.Assert.assertFalse;
34-
import static org.junit.Assert.assertNotNull;
3533

3634
public class RestoreSnapshotRequestTests extends AbstractWireSerializingTestCase<RestoreSnapshotRequest> {
3735
private RestoreSnapshotRequest randomState(RestoreSnapshotRequest instance) {
@@ -89,7 +87,8 @@ private RestoreSnapshotRequest randomState(RestoreSnapshotRequest instance) {
8987
randomBoolean(),
9088
randomBoolean(),
9189
instance.indicesOptions().ignoreAliases() == false,
92-
randomBoolean()
90+
randomBoolean(),
91+
false // Specifying views in the restore snapshot request is not supported
9392
)
9493
)
9594
.gatekeeperOptions(IndicesOptions.GatekeeperOptions.builder().allowSelectors(false).includeFailureIndices(true).build())

server/src/test/java/org/elasticsearch/action/support/IndicesOptionsTests.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ public void testToXContent() throws IOException {
345345
randomBoolean(),
346346
randomBoolean(),
347347
randomBoolean(),
348+
randomBoolean(),
348349
randomBoolean()
349350
);
350351
GatekeeperOptions gatekeeperOptions = new GatekeeperOptions(
@@ -384,6 +385,7 @@ public void testFromXContent() throws IOException {
384385
randomBoolean(),
385386
randomBoolean(),
386387
randomBoolean(),
388+
randomBoolean(),
387389
randomBoolean()
388390
);
389391
ConcreteTargetOptions concreteTargetOptions = new ConcreteTargetOptions(randomBoolean());
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.esql;
9+
10+
/**
11+
* Exposes ES|QL view action names for RBACEngine.
12+
*/
13+
public class EsqlViewActionNames {
14+
public static final String ESQL_PUT_VIEW_ACTION_NAME = "indices:admin/esql/view/put";
15+
public static final String ESQL_GET_VIEW_ACTION_NAME = "indices:admin/esql/view/get";
16+
public static final String ESQL_DELETE_VIEW_ACTION_NAME = "indices:admin/esql/view/delete";
17+
}

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.elasticsearch.xpack.core.ccr.action.ForgetFollowerAction;
4040
import org.elasticsearch.xpack.core.ccr.action.PutFollowAction;
4141
import org.elasticsearch.xpack.core.ccr.action.UnfollowAction;
42+
import org.elasticsearch.xpack.core.esql.EsqlViewActionNames;
4243
import org.elasticsearch.xpack.core.ilm.action.ExplainLifecycleAction;
4344
import org.elasticsearch.xpack.core.inference.action.GetInferenceFieldsInternalAction;
4445
import org.elasticsearch.xpack.core.rollup.action.GetRollupIndexCapsAction;
@@ -194,6 +195,10 @@ public final class IndexPrivilege extends Privilege {
194195
"internal:transport/proxy/indices:internal/admin/ccr/restore/session/clear*",
195196
"internal:transport/proxy/indices:internal/admin/ccr/restore/file_chunk/get*"
196197
);
198+
private static final Automaton CREATE_VIEW_AUTOMATON = patterns(EsqlViewActionNames.ESQL_PUT_VIEW_ACTION_NAME);
199+
private static final Automaton READ_VIEW_METADATA_AUTOMATON = patterns(EsqlViewActionNames.ESQL_GET_VIEW_ACTION_NAME);
200+
private static final Automaton DELETE_VIEW_AUTOMATON = patterns(EsqlViewActionNames.ESQL_DELETE_VIEW_ACTION_NAME);
201+
private static final Automaton MANAGE_VIEW_AUTOMATON = patterns("indices:admin/esql/view*");
197202

198203
public static final IndexPrivilege NONE = new IndexPrivilege("none", Automatons.EMPTY);
199204
public static final IndexPrivilege ALL = new IndexPrivilege("all", ALL_AUTOMATON, IndexComponentSelectorPredicate.ALL);
@@ -231,6 +236,10 @@ public final class IndexPrivilege extends Privilege {
231236
"cross_cluster_replication_internal",
232237
CROSS_CLUSTER_REPLICATION_INTERNAL_AUTOMATON
233238
);
239+
public static final IndexPrivilege MANAGE_VIEW = new IndexPrivilege("manage_view", MANAGE_VIEW_AUTOMATON);
240+
public static final IndexPrivilege CREATE_VIEW = new IndexPrivilege("create_view", CREATE_VIEW_AUTOMATON);
241+
public static final IndexPrivilege DELETE_VIEW = new IndexPrivilege("delete_view", DELETE_VIEW_AUTOMATON);
242+
public static final IndexPrivilege READ_VIEW_METADATA = new IndexPrivilege("read_view_metadata", READ_VIEW_METADATA_AUTOMATON);
234243

235244
public static final IndexPrivilege READ_FAILURE_STORE = new IndexPrivilege(
236245
"read_failure_store",
@@ -275,7 +284,11 @@ public final class IndexPrivilege extends Privilege {
275284
entry("maintenance", MAINTENANCE),
276285
entry("auto_configure", AUTO_CONFIGURE),
277286
entry("cross_cluster_replication", CROSS_CLUSTER_REPLICATION),
278-
entry("cross_cluster_replication_internal", CROSS_CLUSTER_REPLICATION_INTERNAL)
287+
entry("cross_cluster_replication_internal", CROSS_CLUSTER_REPLICATION_INTERNAL),
288+
entry("manage_view", MANAGE_VIEW),
289+
entry("create_view", CREATE_VIEW),
290+
entry("delete_view", DELETE_VIEW),
291+
entry("read_view_metadata", READ_VIEW_METADATA)
279292
).collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue))
280293
)
281294
);

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.elasticsearch.common.Strings;
1717
import org.elasticsearch.common.util.iterable.Iterables;
1818
import org.elasticsearch.test.ESTestCase;
19+
import org.elasticsearch.xpack.core.esql.EsqlViewActionNames;
1920
import org.elasticsearch.xpack.core.rollup.action.GetRollupIndexCapsAction;
2021
import org.elasticsearch.xpack.core.security.support.Automatons;
2122
import org.elasticsearch.xpack.core.transform.action.GetCheckpointAction;
@@ -71,6 +72,18 @@ public void testFindPrivilegesThatGrant() {
7172
equalTo(List.of("monitor", "cross_cluster_replication", "manage", "all"))
7273
);
7374
assertThat(findPrivilegesThatGrant(RefreshAction.NAME), equalTo(List.of("maintenance", "manage", "all")));
75+
assertThat(
76+
findPrivilegesThatGrant(EsqlViewActionNames.ESQL_PUT_VIEW_ACTION_NAME),
77+
equalTo(List.of("create_view", "manage_view", "manage", "all"))
78+
);
79+
assertThat(
80+
findPrivilegesThatGrant(EsqlViewActionNames.ESQL_GET_VIEW_ACTION_NAME),
81+
equalTo(List.of("read_view_metadata", "manage_view", "manage", "all"))
82+
);
83+
assertThat(
84+
findPrivilegesThatGrant(EsqlViewActionNames.ESQL_DELETE_VIEW_ACTION_NAME),
85+
equalTo(List.of("delete_view", "manage_view", "manage", "all"))
86+
);
7487

7588
Predicate<IndexPrivilege> failuresOnly = p -> p.getSelectorPredicate() == IndexComponentSelectorPredicate.FAILURES;
7689
assertThat(findPrivilegesThatGrant(TransportSearchAction.TYPE.name(), failuresOnly), equalTo(List.of("read_failure_store")));
@@ -114,6 +127,26 @@ public void testGet() {
114127
assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true));
115128
assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL));
116129
}
130+
{
131+
IndexPrivilege actual = IndexPrivilege.get("create_view");
132+
assertThat(actual, equalTo(IndexPrivilege.CREATE_VIEW));
133+
assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA));
134+
}
135+
{
136+
IndexPrivilege actual = IndexPrivilege.get("delete_view");
137+
assertThat(actual, equalTo(IndexPrivilege.DELETE_VIEW));
138+
assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA));
139+
}
140+
{
141+
IndexPrivilege actual = IndexPrivilege.get("read_view_metadata");
142+
assertThat(actual, equalTo(IndexPrivilege.READ_VIEW_METADATA));
143+
assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA));
144+
}
145+
{
146+
IndexPrivilege actual = IndexPrivilege.get("manage_view");
147+
assertThat(actual, equalTo(IndexPrivilege.MANAGE_VIEW));
148+
assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA));
149+
}
117150
}
118151

119152
public void testResolveSameSelectorPrivileges() {
@@ -395,6 +428,43 @@ public void testCrossClusterReplicationPrivileges() {
395428
);
396429
}
397430

431+
public void testViewPrivileges() {
432+
final IndexPrivilege createView = resolvePrivilegeAndAssertSingleton(Set.of("create_view"));
433+
assertThat(createView.predicate.test(EsqlViewActionNames.ESQL_PUT_VIEW_ACTION_NAME), is(true));
434+
assertThat(createView.predicate.test(EsqlViewActionNames.ESQL_PUT_VIEW_ACTION_NAME + randomAlphaOfLengthBetween(1, 8)), is(false));
435+
assertThat(createView.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA));
436+
437+
final IndexPrivilege deleteView = resolvePrivilegeAndAssertSingleton(Set.of("delete_view"));
438+
assertThat(deleteView.predicate.test(EsqlViewActionNames.ESQL_DELETE_VIEW_ACTION_NAME), is(true));
439+
assertThat(
440+
deleteView.predicate.test(EsqlViewActionNames.ESQL_DELETE_VIEW_ACTION_NAME + randomAlphaOfLengthBetween(1, 8)),
441+
is(false)
442+
);
443+
assertThat(deleteView.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA));
444+
445+
final IndexPrivilege readViewMetadata = resolvePrivilegeAndAssertSingleton(Set.of("read_view_metadata"));
446+
assertThat(readViewMetadata.predicate.test(EsqlViewActionNames.ESQL_GET_VIEW_ACTION_NAME), is(true));
447+
assertThat(
448+
readViewMetadata.predicate.test(EsqlViewActionNames.ESQL_GET_VIEW_ACTION_NAME + randomAlphaOfLengthBetween(1, 8)),
449+
is(false)
450+
);
451+
assertThat(readViewMetadata.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA));
452+
453+
final IndexPrivilege manageView = resolvePrivilegeAndAssertSingleton(Set.of("manage_view"));
454+
assertThat(manageView.predicate.test(EsqlViewActionNames.ESQL_PUT_VIEW_ACTION_NAME), is(true));
455+
assertThat(manageView.predicate.test(EsqlViewActionNames.ESQL_GET_VIEW_ACTION_NAME), is(true));
456+
assertThat(manageView.predicate.test(EsqlViewActionNames.ESQL_DELETE_VIEW_ACTION_NAME), is(true));
457+
assertThat(manageView.predicate.test("indices:admin/esql/view/other"), is(true));
458+
assertThat(manageView.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA));
459+
460+
assertThat(Automatons.subsetOf(createView.automaton, manageView.automaton), is(true));
461+
assertThat(Automatons.subsetOf(deleteView.automaton, manageView.automaton), is(true));
462+
assertThat(Automatons.subsetOf(readViewMetadata.automaton, manageView.automaton), is(true));
463+
464+
assertThat(Automatons.subsetOf(manageView.automaton, resolvePrivilegeAndAssertSingleton(Set.of("all")).automaton), is(true));
465+
assertThat(Automatons.subsetOf(manageView.automaton, resolvePrivilegeAndAssertSingleton(Set.of("manage")).automaton), is(true));
466+
}
467+
398468
public void testInvalidPrivilegeErrorMessage() {
399469
final String unknownPrivilege = randomValueOtherThanMany(
400470
i -> IndexPrivilege.values().containsKey(i),

0 commit comments

Comments
 (0)