Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Role Mapping Cleanup Security Migration #114830

Closed
wants to merge 44 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
c23b783
Add security migration for cleaning up ECK role mappings
jfreden Oct 4, 2024
157be76
Update docs/changelog/114830.yaml
jfreden Oct 16, 2024
c4ad317
fixup! Use new API for role mapping cleanup
jfreden Oct 17, 2024
dda3c77
fixup! Test
jfreden Oct 17, 2024
c6e54e6
fixup! Test
jfreden Oct 17, 2024
e018eaf
fixup! Refactor and add tests
jfreden Oct 17, 2024
3a7e61f
fixup! Fix cleanup test
jfreden Oct 17, 2024
8b50d79
fixup! Add comments
jfreden Oct 17, 2024
d3259d5
fixup! merge build.gradle
jfreden Oct 17, 2024
7299d19
fixup! another build.gradle
jfreden Oct 17, 2024
11f775a
fixup! Formatting
jfreden Oct 17, 2024
97abe3c
fixup! comments
jfreden Oct 17, 2024
4d4f412
Merge remote-tracking branch 'upstream/8.16' into add_eck_migration_8_16
jfreden Oct 21, 2024
3974dce
fixup! Code review comments
jfreden Oct 23, 2024
52146ed
fixup! Code review comment
jfreden Oct 23, 2024
e16279b
fixup! Spotless :(
jfreden Oct 23, 2024
9c8efe3
fixup! Keep deleting even if something fails
jfreden Oct 23, 2024
1db4315
Merge remote-tracking branch 'upstream/8.16' into add_eck_migration_8_16
jfreden Oct 23, 2024
07db034
fixup! Use new constants
jfreden Oct 23, 2024
d23974f
fixup! Code review comments and bug fixes
jfreden Oct 23, 2024
08c7d95
fixup! typo
jfreden Oct 23, 2024
22ef3b7
fixup! test
jfreden Oct 23, 2024
8762d9b
fixup! lint
jfreden Oct 23, 2024
698b554
fixup! Tests
jfreden Oct 24, 2024
3430c81
Use an enum to track status
jfreden Oct 24, 2024
5a96adf
fixup! checkstyle issue
jfreden Oct 24, 2024
2563993
Merge remote-tracking branch 'upstream/8.16' into add_eck_migration_8_16
jfreden Oct 24, 2024
1709df1
Always include operator file
jfreden Oct 24, 2024
1e01e08
Add another migration test suite
jfreden Oct 24, 2024
90eb45f
fixup! Spotless
jfreden Oct 24, 2024
b86c0a8
fixup! Add assertion on migration status
jfreden Oct 24, 2024
ac9e6da
fixup! Skip calc + test
jfreden Oct 24, 2024
a91f37b
fixup! Review comments and test code
jfreden Oct 25, 2024
ce405e2
Merge remote-tracking branch 'upstream/8.16' into add_eck_migration_8_16
jfreden Oct 25, 2024
304e105
fixup! Test
jfreden Oct 25, 2024
1e65bf7
fixup! Bug
jfreden Oct 25, 2024
9533f6f
Use index setting to track migration version index created on
jfreden Oct 25, 2024
d6e466b
fixup! line
jfreden Oct 28, 2024
9112aba
fixup! Update name
jfreden Oct 28, 2024
a568a31
fixup! Add tests and review comments
jfreden Oct 28, 2024
2ed011b
Revert "Use index setting to track migration version index created on"
jfreden Oct 28, 2024
e1bb9d0
fixup! name
jfreden Oct 28, 2024
3160bd4
fixup! Too much revert
jfreden Oct 28, 2024
9eb0dbe
fixup! Failing tests
jfreden Oct 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ private static IndexVersion def(int id, Version luceneVersion) {
public static final IndexVersion ENABLE_IGNORE_MALFORMED_LOGSDB = def(8_514_00_0, Version.LUCENE_9_11_1);
public static final IndexVersion MERGE_ON_RECOVERY_VERSION = def(8_515_00_0, Version.LUCENE_9_11_1);
public static final IndexVersion UPGRADE_TO_LUCENE_9_12 = def(8_516_00_0, Version.LUCENE_9_12_0);
public static final IndexVersion ADD_ROLE_MAPPING_MIGRATION = def(8_517_00_0, Version.LUCENE_9_12_0);
Copy link
Contributor Author

@jfreden jfreden Oct 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed to allow us to check if an index was created on the same version as the migration. This means that the index was created with all the settings/mappings/docs needed, so we don't need to run the migration (it can be short-circuit and skipped since there is nothing to migrate).


/*
* STOP! READ THIS FIRST! No, really,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@
import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_MIGRATION_FRAMEWORK;
import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_PROFILE_ORIGIN_FEATURE;
import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_ROLES_METADATA_FLATTENED;
import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_ROLE_MAPPING_CLEANUP;
import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.VERSION_SECURITY_PROFILE_ORIGIN;

public class SecurityFeatures implements FeatureSpecification {

@Override
public Set<NodeFeature> getFeatures() {
return Set.of(SECURITY_ROLES_METADATA_FLATTENED, SECURITY_MIGRATION_FRAMEWORK);
return Set.of(SECURITY_ROLE_MAPPING_CLEANUP, SECURITY_ROLES_METADATA_FLATTENED, SECURITY_MIGRATION_FRAMEWORK);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.cluster.metadata.MappingMetadata;
import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.cluster.metadata.ReservedStateMetadata;
import org.elasticsearch.cluster.routing.IndexRoutingTable;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.core.TimeValue;
Expand All @@ -46,6 +47,8 @@
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.threadpool.Scheduler;
import org.elasticsearch.xcontent.XContentType;
import org.elasticsearch.xpack.core.security.authc.support.mapper.ExpressionRoleMapping;
import org.elasticsearch.xpack.core.security.authz.RoleMappingMetadata;
import org.elasticsearch.xpack.security.SecurityFeatures;

import java.time.Instant;
Expand Down Expand Up @@ -74,7 +77,8 @@
public class SecurityIndexManager implements ClusterStateListener {

public static final String SECURITY_VERSION_STRING = "security-version";

private static final String FILE_SETTINGS_METADATA_NAMESPACE = "file_settings";
private static final String HANDLER_ROLE_MAPPINGS_NAME = "role_mappings";
private static final Logger logger = LogManager.getLogger(SecurityIndexManager.class);

/**
Expand Down Expand Up @@ -267,6 +271,22 @@ private static boolean isCreatedOnLatestVersion(IndexMetadata indexMetadata) {
return indexVersionCreated != null && indexVersionCreated.onOrAfter(IndexVersion.current());
}

private static Set<String> getFileSettingsMetadataHandlerRoleMappingKeys(ClusterState clusterState) {
Copy link
Contributor Author

@jfreden jfreden Oct 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't exactly part of the security index, so this doesn't belong here, but I added it here anyway for simplicity. Ideally the security migration state should be tracked in a separate cluster state handler. I think that's an improvement/refactor that should be done in a separate PR if we keep adding migrations on state outside the security index.

The new state handler would still need to handle the main security index manager state, since we rely heavily on state from it in the framework (is the index available for example).

ReservedStateMetadata fileSettingsMetadata = clusterState.metadata().reservedStateMetadata().get(FILE_SETTINGS_METADATA_NAMESPACE);
if (fileSettingsMetadata != null && fileSettingsMetadata.handlers().containsKey(HANDLER_ROLE_MAPPINGS_NAME)) {
return fileSettingsMetadata.handlers().get(HANDLER_ROLE_MAPPINGS_NAME).keys();
}
return Set.of();
}

private static Set<ExpressionRoleMapping> getRoleMappingMetadataMappings(ClusterState clusterState) {
RoleMappingMetadata roleMappingMetadata = RoleMappingMetadata.getFromClusterState(clusterState);
if (roleMappingMetadata != null) {
return roleMappingMetadata.getRoleMappings();
}
return Set.of();
}

@Override
public void clusterChanged(ClusterChangedEvent event) {
if (event.state().blocks().hasGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK)) {
Expand All @@ -284,6 +304,9 @@ public void clusterChanged(ClusterChangedEvent event) {
Tuple<Boolean, Boolean> available = checkIndexAvailable(event.state());
final boolean indexAvailableForWrite = available.v1();
final boolean indexAvailableForSearch = available.v2();
final Set<String> reservedStateRoleMappingNames = getFileSettingsMetadataHandlerRoleMappingKeys(event.state());
final boolean reservedRoleMappingsSynced = reservedStateRoleMappingNames.size() == getRoleMappingMetadataMappings(event.state())
.size();
final boolean mappingIsUpToDate = indexMetadata == null || checkIndexMappingUpToDate(event.state());
final int migrationsVersion = getMigrationVersionFromIndexMetadata(indexMetadata);
final SystemIndexDescriptor.MappingsVersion minClusterMappingVersion = getMinSecurityIndexMappingVersion(event.state());
Expand Down Expand Up @@ -314,6 +337,7 @@ public void clusterChanged(ClusterChangedEvent event) {
indexAvailableForWrite,
mappingIsUpToDate,
createdOnLatestVersion,
reservedRoleMappingsSynced,
migrationsVersion,
minClusterMappingVersion,
indexMappingVersion,
Expand All @@ -323,7 +347,8 @@ public void clusterChanged(ClusterChangedEvent event) {
indexUUID,
allSecurityFeatures.stream()
.filter(feature -> featureService.clusterHasFeature(event.state(), feature))
.collect(Collectors.toSet())
.collect(Collectors.toSet()),
reservedStateRoleMappingNames
);
this.state = newState;

Expand All @@ -334,6 +359,10 @@ public void clusterChanged(ClusterChangedEvent event) {
}
}

public Set<String> getReservedStateRoleMappingNames() {
return state.reservedStateRoleMappingNames;
}

public static int getMigrationVersionFromIndexMetadata(IndexMetadata indexMetadata) {
Map<String, String> customMetadata = indexMetadata == null ? null : indexMetadata.getCustomData(MIGRATION_VERSION_CUSTOM_KEY);
if (customMetadata == null) {
Expand Down Expand Up @@ -438,7 +467,8 @@ private Tuple<Boolean, Boolean> checkIndexAvailable(ClusterState state) {

public boolean isEligibleSecurityMigration(SecurityMigrations.SecurityMigration securityMigration) {
return state.securityFeatures.containsAll(securityMigration.nodeFeaturesRequired())
&& state.indexMappingVersion >= securityMigration.minMappingVersion();
&& state.indexMappingVersion >= securityMigration.minMappingVersion()
&& securityMigration.checkPreConditions(state);
}

public boolean isReadyForSecurityMigration(SecurityMigrations.SecurityMigration securityMigration) {
Expand Down Expand Up @@ -671,13 +701,15 @@ public static class State {
false,
false,
false,
false,
null,
null,
null,
null,
null,
null,
null,
Set.of(),
Set.of()
);
public final Instant creationTime;
Expand All @@ -686,6 +718,7 @@ public static class State {
public final boolean indexAvailableForWrite;
public final boolean mappingUpToDate;
public final boolean createdOnLatestVersion;
public final boolean reservedRoleMappingsSynced;
public final Integer migrationsVersion;
// Min mapping version supported by the descriptors in the cluster
public final SystemIndexDescriptor.MappingsVersion minClusterMappingVersion;
Expand All @@ -696,6 +729,7 @@ public static class State {
public final IndexMetadata.State indexState;
public final String indexUUID;
public final Set<NodeFeature> securityFeatures;
public final Set<String> reservedStateRoleMappingNames;

public State(
Instant creationTime,
Expand All @@ -704,14 +738,16 @@ public State(
boolean indexAvailableForWrite,
boolean mappingUpToDate,
boolean createdOnLatestVersion,
boolean reservedRoleMappingsSynced,
Integer migrationsVersion,
SystemIndexDescriptor.MappingsVersion minClusterMappingVersion,
Integer indexMappingVersion,
String concreteIndexName,
ClusterHealthStatus indexHealth,
IndexMetadata.State indexState,
String indexUUID,
Set<NodeFeature> securityFeatures
Set<NodeFeature> securityFeatures,
Set<String> reservedStateRoleMappingNames
) {
this.creationTime = creationTime;
this.isIndexUpToDate = isIndexUpToDate;
Expand All @@ -720,13 +756,15 @@ public State(
this.mappingUpToDate = mappingUpToDate;
this.migrationsVersion = migrationsVersion;
this.createdOnLatestVersion = createdOnLatestVersion;
this.reservedRoleMappingsSynced = reservedRoleMappingsSynced;
this.minClusterMappingVersion = minClusterMappingVersion;
this.indexMappingVersion = indexMappingVersion;
this.concreteIndexName = concreteIndexName;
this.indexHealth = indexHealth;
this.indexState = indexState;
this.indexUUID = indexUUID;
this.securityFeatures = securityFeatures;
this.reservedStateRoleMappingNames = reservedStateRoleMappingNames;
}

@Override
Expand All @@ -740,13 +778,15 @@ public boolean equals(Object o) {
&& indexAvailableForWrite == state.indexAvailableForWrite
&& mappingUpToDate == state.mappingUpToDate
&& createdOnLatestVersion == state.createdOnLatestVersion
&& reservedRoleMappingsSynced == state.reservedRoleMappingsSynced
&& Objects.equals(indexMappingVersion, state.indexMappingVersion)
&& Objects.equals(migrationsVersion, state.migrationsVersion)
&& Objects.equals(minClusterMappingVersion, state.minClusterMappingVersion)
&& Objects.equals(concreteIndexName, state.concreteIndexName)
&& indexHealth == state.indexHealth
&& indexState == state.indexState
&& Objects.equals(securityFeatures, state.securityFeatures);
&& Objects.equals(securityFeatures, state.securityFeatures)
&& Objects.equals(reservedStateRoleMappingNames, state.reservedStateRoleMappingNames);
}

public boolean indexExists() {
Expand All @@ -762,12 +802,14 @@ public int hashCode() {
indexAvailableForWrite,
mappingUpToDate,
createdOnLatestVersion,
reservedRoleMappingsSynced,
migrationsVersion,
minClusterMappingVersion,
indexMappingVersion,
concreteIndexName,
indexHealth,
securityFeatures
securityFeatures,
reservedStateRoleMappingNames
);
}

Expand All @@ -786,6 +828,8 @@ public String toString() {
+ mappingUpToDate
+ ", createdOnLatestVersion="
+ createdOnLatestVersion
+ ", reservedRoleMappingsSynced="
+ reservedRoleMappingsSynced
+ ", migrationsVersion="
+ migrationsVersion
+ ", minClusterMappingVersion="
Expand All @@ -804,6 +848,8 @@ public String toString() {
+ '\''
+ ", securityFeatures="
+ securityFeatures
+ ", reservedStateRoleMappingNames="
+ reservedStateRoleMappingNames
+ '}';
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.apache.logging.log4j.Logger;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.client.internal.Client;
import org.elasticsearch.features.NodeFeature;
import org.elasticsearch.index.query.BoolQueryBuilder;
Expand All @@ -20,12 +21,23 @@
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.xpack.core.security.action.rolemapping.DeleteRoleMappingAction;
import org.elasticsearch.xpack.core.security.action.rolemapping.DeleteRoleMappingRequestBuilder;
import org.elasticsearch.xpack.core.security.action.rolemapping.GetRoleMappingsAction;
import org.elasticsearch.xpack.core.security.action.rolemapping.GetRoleMappingsRequestBuilder;
import org.elasticsearch.xpack.core.security.authc.support.mapper.ExpressionRoleMapping;

import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import static org.elasticsearch.TransportVersions.ADD_MANAGE_ROLES_PRIVILEGE;
import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN;
import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin;
import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SecurityMainIndexMappingVersion.ADD_REMOTE_CLUSTER_AND_DESCRIPTION_FIELDS;

/**
Expand All @@ -52,6 +64,16 @@ public interface SecurityMigration {
*/
Set<NodeFeature> nodeFeaturesRequired();

/**
* Check that any pre-conditions are met before launching migration
*
* @param securityIndexManagerState current state of the security index
* @return true if pre-conditions met, otherwise false
*/
default boolean checkPreConditions(SecurityIndexManager.State securityIndexManagerState) {
return true;
}

/**
* The min mapping version required to support this migration. This makes sure that the index has at least the min mapping that is
* required to support the migration.
Expand All @@ -62,11 +84,11 @@ public interface SecurityMigration {
}

public static final Integer ROLE_METADATA_FLATTENED_MIGRATION_VERSION = 1;
public static final Integer ROLE_MAPPING_CLEANUP_DUPLICATES = 2;
private static final Logger logger = LogManager.getLogger(SecurityMigration.class);

public static final TreeMap<Integer, SecurityMigration> MIGRATIONS_BY_VERSION = new TreeMap<>(
Map.of(ROLE_METADATA_FLATTENED_MIGRATION_VERSION, new SecurityMigration() {
private static final Logger logger = LogManager.getLogger(SecurityMigration.class);

@Override
public void migrate(SecurityIndexManager indexManager, Client client, ActionListener<Void> listener) {
BoolQueryBuilder filterQuery = new BoolQueryBuilder().filter(QueryBuilders.termQuery("type", "role"))
Expand Down Expand Up @@ -119,6 +141,73 @@ public Set<NodeFeature> nodeFeaturesRequired() {
public int minMappingVersion() {
return ADD_REMOTE_CLUSTER_AND_DESCRIPTION_FIELDS.id();
}
}, ROLE_MAPPING_CLEANUP_DUPLICATES, new SecurityMigration() {
@Override
public void migrate(SecurityIndexManager indexManager, Client client, ActionListener<Void> listener) {
Set<String> clusterStateRoleMappingNames = indexManager.getReservedStateRoleMappingNames();

// No role mappings in cluster state -> no cleanup needed
if (clusterStateRoleMappingNames.isEmpty()) {
listener.onResponse(null);
return;
}

getNativeRoleMappings(client, ActionListener.wrap(roleMappings -> {
logger.info("Found [" + roleMappings.size() + "] role mappings to cleanup in .security index.");
deleteNativeRoleMappings(client, roleMappings.iterator(), listener);
}, listener::onFailure), clusterStateRoleMappingNames.toArray(String[]::new));
}

private void getNativeRoleMappings(Client client, ActionListener<List<String>> listener, String... mappingNames) {
executeAsyncWithOrigin(
client,
SECURITY_ORIGIN,
GetRoleMappingsAction.INSTANCE,
new GetRoleMappingsRequestBuilder(client).names(mappingNames).request(),
ActionListener.wrap(
response -> listener.onResponse(Arrays.stream(response.mappings()).map(ExpressionRoleMapping::getName).toList()),
listener::onFailure
)
);
}

private void deleteNativeRoleMappings(Client client, Iterator<String> namesIterator, ActionListener<Void> listener) {
String name = namesIterator.next();
executeAsyncWithOrigin(
client,
SECURITY_ORIGIN,
DeleteRoleMappingAction.INSTANCE,
new DeleteRoleMappingRequestBuilder(client).name(name).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).request(),
ActionListener.wrap(response -> {
if (response.isFound() == false) {
logger.warn("Expected role mapping [" + name + "] not found during role mapping clean up.");
} else {
logger.info("Deleted duplicated role mapping [" + name + "] from .security index");
}
if (namesIterator.hasNext()) {
deleteNativeRoleMappings(client, namesIterator, listener);
} else {
listener.onResponse(null);
}
}, listener::onFailure)
);
}

@Override
public boolean checkPreConditions(SecurityIndexManager.State securityIndexManagerState) {
// If there are operator defined role mappings, make sure they've been loaded in to cluster state before launching migration
return securityIndexManagerState.reservedRoleMappingsSynced;
}

@Override
public Set<NodeFeature> nodeFeaturesRequired() {
return Set.of(SecuritySystemIndices.SECURITY_ROLE_MAPPING_CLEANUP);
}

@Override
public int minMappingVersion() {
return ADD_MANAGE_ROLES_PRIVILEGE.id();
}
})
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public class SecuritySystemIndices {
public static final NodeFeature SECURITY_PROFILE_ORIGIN_FEATURE = new NodeFeature("security.security_profile_origin");
public static final NodeFeature SECURITY_MIGRATION_FRAMEWORK = new NodeFeature("security.migration_framework");
public static final NodeFeature SECURITY_ROLES_METADATA_FLATTENED = new NodeFeature("security.roles_metadata_flattened");
public static final NodeFeature SECURITY_ROLE_MAPPING_CLEANUP = new NodeFeature("security.role_mapping_cleanup");

/**
* Security managed index mappings used to be updated based on the product version. They are now updated based on per-index mappings
Expand Down
Loading