Skip to content

Commit 5f8dd91

Browse files
authored
Phase 2 support for operator privileges: Cluster settings (#66684) (#68563)
Add a new OperatorDynamic enum to differentiate between operator-only and regular dynamic cluster settings. The Setting constructor validates that Dynamic and OperatorDynamic cannot be both specified. Operator-only settings behave as the follows: * When the feature is enabled, operator-only settings cannot be updated with PUT cluster settings API unless the user is an operator. * The restore behaviour for operator-only settings will be identical for either operator or non-operator user. That is, when operator privileges are enabled, operator-only settings will not be restored. Otherwise (if the feature is disabled), the behaviour is the same as of today.
1 parent adf3d42 commit 5f8dd91

File tree

21 files changed

+612
-58
lines changed

21 files changed

+612
-58
lines changed

server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequest.java

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ public class RestoreSnapshotRequest extends MasterNodeRequest<RestoreSnapshotReq
5656
private Settings indexSettings = EMPTY_SETTINGS;
5757
private String[] ignoreIndexSettings = Strings.EMPTY_ARRAY;
5858

59+
// This field does not get serialised (except toString for debugging purpose) because it is always set locally by authz
60+
private boolean skipOperatorOnlyState = false;
61+
5962
@Nullable // if any snapshot UUID will do
6063
private String snapshotUuid;
6164

@@ -455,6 +458,14 @@ public String snapshotUuid() {
455458
return snapshotUuid;
456459
}
457460

461+
public boolean skipOperatorOnlyState() {
462+
return skipOperatorOnlyState;
463+
}
464+
465+
public void skipOperatorOnlyState(boolean skipOperatorOnlyState) {
466+
this.skipOperatorOnlyState = skipOperatorOnlyState;
467+
}
468+
458469
/**
459470
* Parses restore definition
460471
*
@@ -523,6 +534,12 @@ public RestoreSnapshotRequest source(Map<String, Object> source) {
523534
@Override
524535
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
525536
builder.startObject();
537+
toXContentFragment(builder, params);
538+
builder.endObject();
539+
return builder;
540+
}
541+
542+
private void toXContentFragment(XContentBuilder builder, Params params) throws IOException {
526543
builder.startArray("indices");
527544
for (String index : indices) {
528545
builder.value(index);
@@ -552,8 +569,6 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
552569
builder.value(ignoreIndexSetting);
553570
}
554571
builder.endArray();
555-
builder.endObject();
556-
return builder;
557572
}
558573

559574
@Override
@@ -578,20 +593,27 @@ public boolean equals(Object o) {
578593
Objects.equals(renameReplacement, that.renameReplacement) &&
579594
Objects.equals(indexSettings, that.indexSettings) &&
580595
Arrays.equals(ignoreIndexSettings, that.ignoreIndexSettings) &&
581-
Objects.equals(snapshotUuid, that.snapshotUuid);
596+
Objects.equals(snapshotUuid, that.snapshotUuid) &&
597+
skipOperatorOnlyState == that.skipOperatorOnlyState;
582598
}
583599

584600
@Override
585601
public int hashCode() {
586602
int result = Objects.hash(snapshot, repository, indicesOptions, renamePattern, renameReplacement, waitForCompletion,
587-
includeGlobalState, partial, includeAliases, indexSettings, snapshotUuid);
603+
includeGlobalState, partial, includeAliases, indexSettings, snapshotUuid, skipOperatorOnlyState);
588604
result = 31 * result + Arrays.hashCode(indices);
589605
result = 31 * result + Arrays.hashCode(ignoreIndexSettings);
590606
return result;
591607
}
592608

593609
@Override
594610
public String toString() {
595-
return Strings.toString(this);
611+
return Strings.toString((ToXContentObject) (builder, params) -> {
612+
builder.startObject();
613+
toXContentFragment(builder, params);
614+
builder.field("skipOperatorOnlyState", skipOperatorOnlyState);
615+
builder.endObject();
616+
return builder;
617+
});
596618
}
597619
}

server/src/main/java/org/elasticsearch/common/settings/Setting.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ public enum Property {
8888
*/
8989
Dynamic,
9090

91+
/**
92+
* Operator only Dynamic setting
93+
*/
94+
OperatorDynamic,
95+
9196
/**
9297
* mark this setting as final, not updateable even when the context is not dynamic
9398
* ie. Setting this property on an index scoped setting will fail update when the index is closed
@@ -158,9 +163,13 @@ private Setting(Key key, @Nullable Setting<T> fallbackSetting, Function<Settings
158163
this.properties = EMPTY_PROPERTIES;
159164
} else {
160165
final EnumSet<Property> propertiesAsSet = EnumSet.copyOf(Arrays.asList(properties));
161-
if (propertiesAsSet.contains(Property.Dynamic) && propertiesAsSet.contains(Property.Final)) {
166+
if ((propertiesAsSet.contains(Property.Dynamic) || propertiesAsSet.contains(Property.OperatorDynamic))
167+
&& propertiesAsSet.contains(Property.Final)) {
162168
throw new IllegalArgumentException("final setting [" + key + "] cannot be dynamic");
163169
}
170+
if (propertiesAsSet.contains(Property.Dynamic) && propertiesAsSet.contains(Property.OperatorDynamic)) {
171+
throw new IllegalArgumentException("setting [" + key + "] cannot be both dynamic and operator dynamic");
172+
}
164173
checkPropertyRequiresIndexScope(propertiesAsSet, Property.NotCopyableOnResize);
165174
checkPropertyRequiresIndexScope(propertiesAsSet, Property.InternalIndex);
166175
checkPropertyRequiresIndexScope(propertiesAsSet, Property.PrivateIndex);
@@ -285,7 +294,14 @@ public final Key getRawKey() {
285294
* Returns <code>true</code> if this setting is dynamically updateable, otherwise <code>false</code>
286295
*/
287296
public final boolean isDynamic() {
288-
return properties.contains(Property.Dynamic);
297+
return properties.contains(Property.Dynamic) || properties.contains(Property.OperatorDynamic);
298+
}
299+
300+
/**
301+
* Returns <code>true</code> if this setting is dynamically updateable by operators, otherwise <code>false</code>
302+
*/
303+
public final boolean isOperatorOnly() {
304+
return properties.contains(Property.OperatorDynamic);
289305
}
290306

291307
/**

server/src/main/java/org/elasticsearch/snapshots/RestoreService.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import org.elasticsearch.common.lucene.Lucene;
5656
import org.elasticsearch.common.regex.Regex;
5757
import org.elasticsearch.common.settings.ClusterSettings;
58+
import org.elasticsearch.common.settings.Setting;
5859
import org.elasticsearch.common.settings.Settings;
5960
import org.elasticsearch.index.Index;
6061
import org.elasticsearch.index.IndexSettings;
@@ -81,6 +82,7 @@
8182
import java.util.function.Function;
8283
import java.util.function.Predicate;
8384
import java.util.stream.Collectors;
85+
import java.util.stream.Stream;
8486

8587
import static java.util.Collections.unmodifiableSet;
8688
import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_AUTO_EXPAND_REPLICAS;
@@ -443,6 +445,22 @@ restoreUUID, snapshot, overallState(RestoreInProgress.State.INIT, shards),
443445
if (request.includeGlobalState()) {
444446
if (metadata.persistentSettings() != null) {
445447
Settings settings = metadata.persistentSettings();
448+
if (request.skipOperatorOnlyState()) {
449+
// Skip any operator-only settings from the snapshot. This happens when operator privileges are enabled
450+
Set<String> operatorSettingKeys = Stream.concat(
451+
settings.keySet().stream(), currentState.metadata().persistentSettings().keySet().stream())
452+
.filter(k -> {
453+
final Setting<?> setting = clusterSettings.get(k);
454+
return setting != null && setting.isOperatorOnly();
455+
})
456+
.collect(Collectors.toSet());
457+
if (false == operatorSettingKeys.isEmpty()) {
458+
settings = Settings.builder()
459+
.put(settings.filter(k -> false == operatorSettingKeys.contains(k)))
460+
.put(currentState.metadata().persistentSettings().filter(operatorSettingKeys::contains))
461+
.build();
462+
}
463+
}
446464
clusterSettings.validateUpdate(settings);
447465
mdBuilder.persistentSettings(settings);
448466
}
@@ -457,6 +475,7 @@ restoreUUID, snapshot, overallState(RestoreInProgress.State.INIT, shards),
457475
if (RepositoriesMetadata.TYPE.equals(cursor.key) == false
458476
&& DataStreamMetadata.TYPE.equals(cursor.key) == false
459477
&& cursor.value instanceof Metadata.NonRestorableCustom == false) {
478+
// TODO: Check request.skipOperatorOnly for Autoscaling policies (NonRestorableCustom)
460479
// Don't restore repositories while we are working with them
461480
// TODO: Should we restore them at the end?
462481
// Also, don't restore data streams here, we already added them to the metadata builder above

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import org.elasticsearch.action.support.IndicesOptions;
1212
import org.elasticsearch.common.bytes.BytesReference;
13+
import org.elasticsearch.common.io.stream.BytesStreamOutput;
1314
import org.elasticsearch.common.io.stream.Writeable;
1415
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
1516
import org.elasticsearch.common.xcontent.ToXContent;
@@ -29,6 +30,8 @@
2930
import java.util.List;
3031
import java.util.Map;
3132

33+
import static org.hamcrest.Matchers.containsString;
34+
3235
public class RestoreSnapshotRequestTests extends AbstractWireSerializingTestCase<RestoreSnapshotRequest> {
3336
private RestoreSnapshotRequest randomState(RestoreSnapshotRequest instance) {
3437
if (randomBoolean()) {
@@ -123,4 +126,37 @@ public void testSource() throws IOException {
123126

124127
assertEquals(original, processed);
125128
}
129+
130+
public void testSkipOperatorOnlyWillNotBeSerialised() throws IOException {
131+
RestoreSnapshotRequest original = createTestInstance();
132+
assertFalse(original.skipOperatorOnlyState()); // default is false
133+
if (randomBoolean()) {
134+
original.skipOperatorOnlyState(true);
135+
}
136+
Map<String, Object> map = convertRequestToMap(original);
137+
// It is not serialised as xcontent
138+
assertFalse(map.containsKey("skip_operator_only"));
139+
140+
// Xcontent is not affected by the value of skipOperatorOnlyState
141+
original.skipOperatorOnlyState(original.skipOperatorOnlyState() == false);
142+
assertEquals(map, convertRequestToMap(original));
143+
144+
// Nor does it serialise to streamInput
145+
final BytesStreamOutput streamOutput = new BytesStreamOutput();
146+
original.writeTo(streamOutput);
147+
final RestoreSnapshotRequest deserialized = new RestoreSnapshotRequest(streamOutput.bytes().streamInput());
148+
assertFalse(deserialized.skipOperatorOnlyState());
149+
}
150+
151+
public void testToStringWillIncludeSkipOperatorOnlyState() {
152+
RestoreSnapshotRequest original = createTestInstance();
153+
assertThat(original.toString(), containsString("skipOperatorOnlyState"));
154+
}
155+
156+
private Map<String, Object> convertRequestToMap(RestoreSnapshotRequest request) throws IOException {
157+
XContentBuilder builder = request.toXContent(XContentFactory.jsonBuilder(), new ToXContent.MapParams(Collections.emptyMap()));
158+
XContentParser parser = XContentType.JSON.xContent().createParser(
159+
NamedXContentRegistry.EMPTY, null, BytesReference.bytes(builder).streamInput());
160+
return parser.mapOrdered();
161+
}
126162
}

server/src/test/java/org/elasticsearch/common/settings/SettingTests.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -939,10 +939,16 @@ public void testRejectNullProperties() {
939939

940940
public void testRejectConflictingDynamicAndFinalProperties() {
941941
IllegalArgumentException ex = expectThrows(IllegalArgumentException.class,
942-
() -> Setting.simpleString("foo.bar", Property.Final, Property.Dynamic));
942+
() -> Setting.simpleString("foo.bar", Property.Final, randomFrom(Property.Dynamic, Property.OperatorDynamic)));
943943
assertThat(ex.getMessage(), containsString("final setting [foo.bar] cannot be dynamic"));
944944
}
945945

946+
public void testRejectConflictingDynamicAndOperatorDynamicProperties() {
947+
IllegalArgumentException ex = expectThrows(IllegalArgumentException.class,
948+
() -> Setting.simpleString("foo.bar", Property.Dynamic, Property.OperatorDynamic));
949+
assertThat(ex.getMessage(), containsString("setting [foo.bar] cannot be both dynamic and operator dynamic"));
950+
}
951+
946952
public void testRejectNonIndexScopedNotCopyableOnResizeSetting() {
947953
final IllegalArgumentException e = expectThrows(
948954
IllegalArgumentException.class,
@@ -1233,4 +1239,11 @@ public boolean innerMatch(LogEvent event) {
12331239
mockLogAppender.stop();
12341240
}
12351241
}
1242+
1243+
public void testDynamicTest() {
1244+
final Property property = randomFrom(Property.Dynamic, Property.OperatorDynamic);
1245+
final Setting<String> setting = Setting.simpleString("foo.bar", property);
1246+
assertTrue(setting.isDynamic());
1247+
assertEquals(setting.isOperatorOnly(), property == Property.OperatorDynamic);
1248+
}
12361249
}

x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ public Set<DiscoveryNodeRole> getRoles() {
401401
public static final String MACHINE_MEMORY_NODE_ATTR = "ml.machine_memory";
402402
public static final String MAX_JVM_SIZE_NODE_ATTR = "ml.max_jvm_size";
403403
public static final Setting<Integer> CONCURRENT_JOB_ALLOCATIONS =
404-
Setting.intSetting("xpack.ml.node_concurrent_job_allocations", 2, 0, Property.Dynamic, Property.NodeScope);
404+
Setting.intSetting("xpack.ml.node_concurrent_job_allocations", 2, 0, Property.OperatorDynamic, Property.NodeScope);
405405
/**
406406
* The amount of memory needed to load the ML native code shared libraries. The assumption is that the first
407407
* ML job to run on a given node will do this, and then subsequent ML jobs on the same node will reuse the
@@ -415,7 +415,7 @@ public Set<DiscoveryNodeRole> getRoles() {
415415
// controls the types of jobs that can be created, and each job alone is considerably smaller than what each node
416416
// can handle.
417417
public static final Setting<Integer> MAX_MACHINE_MEMORY_PERCENT =
418-
Setting.intSetting("xpack.ml.max_machine_memory_percent", 30, 5, 200, Property.Dynamic, Property.NodeScope);
418+
Setting.intSetting("xpack.ml.max_machine_memory_percent", 30, 5, 200, Property.OperatorDynamic, Property.NodeScope);
419419
/**
420420
* This boolean value indicates if `max_machine_memory_percent` should be ignored and a automatic calculation is used instead.
421421
*
@@ -431,10 +431,10 @@ public Set<DiscoveryNodeRole> getRoles() {
431431
public static final Setting<Boolean> USE_AUTO_MACHINE_MEMORY_PERCENT = Setting.boolSetting(
432432
"xpack.ml.use_auto_machine_memory_percent",
433433
false,
434-
Property.Dynamic,
434+
Property.OperatorDynamic,
435435
Property.NodeScope);
436436
public static final Setting<Integer> MAX_LAZY_ML_NODES =
437-
Setting.intSetting("xpack.ml.max_lazy_ml_nodes", 0, 0, Property.Dynamic, Property.NodeScope);
437+
Setting.intSetting("xpack.ml.max_lazy_ml_nodes", 0, 0, Property.OperatorDynamic, Property.NodeScope);
438438

439439
// Before 8.0.0 this needs to match the max allowed value for xpack.ml.max_open_jobs,
440440
// as the current node could be running in a cluster where some nodes are still using
@@ -449,7 +449,7 @@ public Set<DiscoveryNodeRole> getRoles() {
449449

450450
public static final Setting<TimeValue> PROCESS_CONNECT_TIMEOUT =
451451
Setting.timeSetting("xpack.ml.process_connect_timeout", TimeValue.timeValueSeconds(10),
452-
TimeValue.timeValueSeconds(5), Setting.Property.Dynamic, Setting.Property.NodeScope);
452+
TimeValue.timeValueSeconds(5), Property.OperatorDynamic, Setting.Property.NodeScope);
453453

454454
// Undocumented setting for integration test purposes
455455
public static final Setting<ByteSizeValue> MIN_DISK_SPACE_OFF_HEAP =
@@ -468,7 +468,7 @@ public Set<DiscoveryNodeRole> getRoles() {
468468
}
469469
return value;
470470
},
471-
Property.Dynamic,
471+
Property.OperatorDynamic,
472472
Property.NodeScope
473473
);
474474

@@ -481,7 +481,7 @@ public Set<DiscoveryNodeRole> getRoles() {
481481
public static final Setting<ByteSizeValue> MAX_ML_NODE_SIZE = Setting.byteSizeSetting(
482482
"xpack.ml.max_ml_node_size",
483483
ByteSizeValue.ZERO,
484-
Property.Dynamic,
484+
Property.OperatorDynamic,
485485
Property.NodeScope);
486486

487487
private static final Logger logger = LogManager.getLogger(MachineLearning.class);

x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MlConfigMigrationEligibilityCheck.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public class MlConfigMigrationEligibilityCheck {
2727
private static final Version MIN_NODE_VERSION = Version.V_6_6_0;
2828

2929
public static final Setting<Boolean> ENABLE_CONFIG_MIGRATION = Setting.boolSetting(
30-
"xpack.ml.enable_config_migration", true, Setting.Property.Dynamic, Setting.Property.NodeScope);
30+
"xpack.ml.enable_config_migration", true, Setting.Property.OperatorDynamic, Setting.Property.NodeScope);
3131

3232
private volatile boolean isConfigMigrationEnabled;
3333

x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/persistence/ResultsPersisterService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public class ResultsPersisterService {
7777
20,
7878
0,
7979
50,
80-
Setting.Property.Dynamic,
80+
Setting.Property.OperatorDynamic,
8181
Setting.Property.NodeScope);
8282
private static final int MAX_RETRY_SLEEP_MILLIS = (int)Duration.ofMinutes(15).toMillis();
8383
private static final int MIN_RETRY_SLEEP_MILLIS = 50;

x-pack/plugin/security/qa/operator-privileges-tests/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ dependencies {
1616
javaRestTestImplementation project.sourceSets.main.runtimeClasspath
1717
}
1818

19+
File repoDir = file("$buildDir/testclusters/repo")
20+
21+
javaRestTest {
22+
/* To support taking snapshots, we have to set path.repo setting */
23+
systemProperty 'tests.path.repo', repoDir
24+
}
25+
1926
testClusters.all {
2027
testDistribution = 'DEFAULT'
2128
numberOfNodes = 3
@@ -27,6 +34,7 @@ testClusters.all {
2734
setting 'xpack.security.enabled', 'true'
2835
setting 'xpack.security.http.ssl.enabled', 'false'
2936
setting 'xpack.security.operator_privileges.enabled', "true"
37+
setting 'path.repo', repoDir.absolutePath
3038

3139
user username: "test_admin", password: 'x-pack-test-password', role: "superuser"
3240
user username: "test_operator", password: 'x-pack-test-password', role: "limited_operator"

0 commit comments

Comments
 (0)