Skip to content

Commit 0dbc3c7

Browse files
committed
Add ECK Role Mapping Cleanup (elastic#115823)
* Add security migration for cleaning up ECK role mappings (cherry picked from commit a7031d8) # Conflicts: # server/src/main/java/org/elasticsearch/index/IndexVersions.java # x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityMigrations.java # x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/AbstractUpgradeTestCase.java
1 parent ba26394 commit 0dbc3c7

File tree

25 files changed

+1289
-82
lines changed

25 files changed

+1289
-82
lines changed

docs/changelog/115823.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 115823
2+
summary: Add ECK Role Mapping Cleanup
3+
area: Security
4+
type: bug
5+
issues: []

qa/rolling-upgrade/src/javaRestTest/java/org/elasticsearch/upgrades/FileSettingsRoleMappingUpgradeIT.java

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,20 @@
2323
import org.junit.rules.TemporaryFolder;
2424
import org.junit.rules.TestRule;
2525

26-
import java.io.IOException;
2726
import java.util.List;
27+
import java.util.Map;
2828
import java.util.function.Supplier;
2929

30+
import static org.hamcrest.Matchers.contains;
3031
import static org.hamcrest.Matchers.equalTo;
31-
import static org.hamcrest.Matchers.hasItem;
3232
import static org.hamcrest.Matchers.is;
3333
import static org.hamcrest.Matchers.not;
3434
import static org.hamcrest.Matchers.nullValue;
3535

3636
public class FileSettingsRoleMappingUpgradeIT extends ParameterizedRollingUpgradeTestCase {
3737

38-
private static final String settingsJSON = """
38+
private static final int ROLE_MAPPINGS_CLEANUP_MIGRATION_VERSION = 2;
39+
private static final String SETTING_JSON = """
3940
{
4041
"metadata": {
4142
"version": "1",
@@ -53,7 +54,6 @@ public class FileSettingsRoleMappingUpgradeIT extends ParameterizedRollingUpgrad
5354
}""";
5455

5556
private static final TemporaryFolder repoDirectory = new TemporaryFolder();
56-
5757
private static final ElasticsearchCluster cluster = ElasticsearchCluster.local()
5858
.distribution(DistributionType.DEFAULT)
5959
.version(getOldClusterTestVersion())
@@ -68,7 +68,7 @@ public String get() {
6868
.setting("xpack.security.enabled", "true")
6969
// workaround to avoid having to set up clients and authorization headers
7070
.setting("xpack.security.authc.anonymous.roles", "superuser")
71-
.configFile("operator/settings.json", Resource.fromString(settingsJSON))
71+
.configFile("operator/settings.json", Resource.fromString(SETTING_JSON))
7272
.build();
7373

7474
@ClassRule
@@ -91,7 +91,30 @@ public void checkVersions() {
9191
);
9292
}
9393

94-
public void testRoleMappingsAppliedOnUpgrade() throws IOException {
94+
private static void waitForSecurityMigrationCompletionIfIndexExists() throws Exception {
95+
final Request request = new Request("GET", "_cluster/state/metadata/.security-7");
96+
assertBusy(() -> {
97+
Map<String, Object> indices = new XContentTestUtils.JsonMapView(entityAsMap(client().performRequest(request))).get(
98+
"metadata.indices"
99+
);
100+
assertNotNull(indices);
101+
// If the security index exists, migration needs to happen. There is a bug in pre cluster state role mappings code that tries
102+
// to write file based role mappings before security index manager state is recovered, this makes it look like the security
103+
// index is outdated (isIndexUpToDate == false). Because we can't rely on the index being there for old versions, this check
104+
// is needed.
105+
if (indices.containsKey(".security-7")) {
106+
// JsonMapView doesn't support . prefixed indices (splits on .)
107+
@SuppressWarnings("unchecked")
108+
String responseVersion = new XContentTestUtils.JsonMapView((Map<String, Object>) indices.get(".security-7")).get(
109+
"migration_version.version"
110+
);
111+
assertNotNull(responseVersion);
112+
assertTrue(Integer.parseInt(responseVersion) >= ROLE_MAPPINGS_CLEANUP_MIGRATION_VERSION);
113+
}
114+
});
115+
}
116+
117+
public void testRoleMappingsAppliedOnUpgrade() throws Exception {
95118
if (isOldCluster()) {
96119
Request clusterStateRequest = new Request("GET", "/_cluster/state/metadata");
97120
List<Object> roleMappings = new XContentTestUtils.JsonMapView(entityAsMap(client().performRequest(clusterStateRequest))).get(
@@ -107,11 +130,10 @@ public void testRoleMappingsAppliedOnUpgrade() throws IOException {
107130
).get("metadata.role_mappings.role_mappings");
108131
assertThat(clusterStateRoleMappings, is(not(nullValue())));
109132
assertThat(clusterStateRoleMappings.size(), equalTo(1));
110-
133+
waitForSecurityMigrationCompletionIfIndexExists();
111134
assertThat(
112135
entityAsMap(client().performRequest(new Request("GET", "/_security/role_mapping"))).keySet(),
113-
// TODO change this to `contains` once the clean-up migration work is merged
114-
hasItem("everyone_kibana-read-only-operator-mapping")
136+
contains("everyone_kibana-read-only-operator-mapping")
115137
);
116138
}
117139
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,21 @@ public Set<String> conflicts(String handlerName, Set<String> modified) {
9191
return Collections.unmodifiableSet(intersect);
9292
}
9393

94+
/**
95+
* Get the reserved keys for the handler name
96+
*
97+
* @param handlerName handler name to get keys for
98+
* @return set of keys for that handler
99+
*/
100+
public Set<String> keys(String handlerName) {
101+
ReservedStateHandlerMetadata handlerMetadata = handlers.get(handlerName);
102+
if (handlerMetadata == null || handlerMetadata.keys().isEmpty()) {
103+
return Collections.emptySet();
104+
}
105+
106+
return Collections.unmodifiableSet(handlerMetadata.keys());
107+
}
108+
94109
/**
95110
* Reads an {@link ReservedStateMetadata} from a {@link StreamInput}
96111
*

server/src/main/java/org/elasticsearch/index/IndexVersions.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ private static IndexVersion def(int id, Version luceneVersion) {
118118
public static final IndexVersion MERGE_ON_RECOVERY_VERSION = def(8_515_00_0, Version.LUCENE_9_11_1);
119119
public static final IndexVersion UPGRADE_TO_LUCENE_9_12 = def(8_516_00_0, Version.LUCENE_9_12_0);
120120
public static final IndexVersion ENABLE_IGNORE_ABOVE_LOGSDB = def(8_517_00_0, Version.LUCENE_9_12_0);
121+
public static final IndexVersion ADD_ROLE_MAPPING_CLEANUP_MIGRATION = def(8_518_00_0, Version.LUCENE_9_12_0);
122+
121123
/*
122124
* STOP! READ THIS FIRST! No, really,
123125
* ____ _____ ___ ____ _ ____ _____ _ ____ _____ _ _ ___ ____ _____ ___ ____ ____ _____ _

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/mapper/ExpressionRoleMapping.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,15 +206,15 @@ public RoleMapperExpression getExpression() {
206206
* that match the {@link #getExpression() expression} in this mapping.
207207
*/
208208
public List<String> getRoles() {
209-
return Collections.unmodifiableList(roles);
209+
return roles != null ? Collections.unmodifiableList(roles) : Collections.emptyList();
210210
}
211211

212212
/**
213213
* The list of {@link RoleDescriptor roles} (specified by a {@link TemplateRoleName template} that evaluates to one or more names)
214214
* that should be assigned to users that match the {@link #getExpression() expression} in this mapping.
215215
*/
216216
public List<TemplateRoleName> getRoleTemplates() {
217-
return Collections.unmodifiableList(roleTemplates);
217+
return roleTemplates != null ? Collections.unmodifiableList(roleTemplates) : Collections.emptyList();
218218
}
219219

220220
/**
@@ -223,7 +223,7 @@ public List<TemplateRoleName> getRoleTemplates() {
223223
* This is not used within the mapping process, and does not affect whether the expression matches, nor which roles are assigned.
224224
*/
225225
public Map<String, Object> getMetadata() {
226-
return Collections.unmodifiableMap(metadata);
226+
return metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap();
227227
}
228228

229229
/**
@@ -233,6 +233,15 @@ public boolean isEnabled() {
233233
return enabled;
234234
}
235235

236+
/**
237+
* Whether this mapping is an operator defined/read only role mapping
238+
*/
239+
public boolean isReadOnly() {
240+
return metadata != null && metadata.get(ExpressionRoleMapping.READ_ONLY_ROLE_MAPPING_METADATA_FLAG) instanceof Boolean readOnly
241+
? readOnly
242+
: false;
243+
}
244+
236245
@Override
237246
public String toString() {
238247
return getClass().getSimpleName() + "<" + name + " ; " + roles + "/" + roleTemplates + " = " + Strings.toString(expression) + ">";

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,14 @@ public static boolean hasFallbackName(ExpressionRoleMapping expressionRoleMappin
191191
return expressionRoleMapping.getName().equals(FALLBACK_NAME);
192192
}
193193

194+
/**
195+
* Check if any of the role mappings have a fallback name
196+
* @return true if any role mappings have the fallback name
197+
*/
198+
public boolean hasAnyMappingWithFallbackName() {
199+
return roleMappings.stream().anyMatch(RoleMappingMetadata::hasFallbackName);
200+
}
201+
194202
/**
195203
* Parse a role mapping from XContent, restoring the name from a reserved metadata field.
196204
* Used to parse a role mapping annotated with its name in metadata via @see {@link #copyWithNameInMetadata(ExpressionRoleMapping)}.

x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/integration/RoleMappingFileSettingsIT.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,29 @@ public void clusterChanged(ClusterChangedEvent event) {
204204
return new Tuple<>(savedClusterState, metadataVersion);
205205
}
206206

207+
// Wait for any file metadata
208+
public static Tuple<CountDownLatch, AtomicLong> setupClusterStateListener(String node) {
209+
ClusterService clusterService = internalCluster().clusterService(node);
210+
CountDownLatch savedClusterState = new CountDownLatch(1);
211+
AtomicLong metadataVersion = new AtomicLong(-1);
212+
clusterService.addListener(new ClusterStateListener() {
213+
@Override
214+
public void clusterChanged(ClusterChangedEvent event) {
215+
ReservedStateMetadata reservedState = event.state().metadata().reservedStateMetadata().get(FileSettingsService.NAMESPACE);
216+
if (reservedState != null) {
217+
ReservedStateHandlerMetadata handlerMetadata = reservedState.handlers().get(ReservedRoleMappingAction.NAME);
218+
if (handlerMetadata != null) {
219+
clusterService.removeListener(this);
220+
metadataVersion.set(event.state().metadata().version());
221+
savedClusterState.countDown();
222+
}
223+
}
224+
}
225+
});
226+
227+
return new Tuple<>(savedClusterState, metadataVersion);
228+
}
229+
207230
public static Tuple<CountDownLatch, AtomicLong> setupClusterStateListenerForCleanup(String node) {
208231
ClusterService clusterService = internalCluster().clusterService(node);
209232
CountDownLatch savedClusterState = new CountDownLatch(1);

0 commit comments

Comments
 (0)