Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -800,10 +800,7 @@ Collection<Object> createComponents(
this.persistentTasksService.set(persistentTasksService);

systemIndices.getMainIndexManager().addStateListener((oldState, newState) -> {
// Only consider applying migrations if it's the master node and the security index exists
if (clusterService.state().nodes().isLocalNodeElectedMaster() && newState.indexExists()) {
applyPendingSecurityMigrations(newState);
}
applyPendingSecurityMigrations(clusterService.state(), newState);
});

scriptServiceReference.set(scriptService);
Expand Down Expand Up @@ -1223,7 +1220,12 @@ Collection<Object> createComponents(
return components;
}

private void applyPendingSecurityMigrations(SecurityIndexManager.State newState) {
private void applyPendingSecurityMigrations(ClusterState clusterState, SecurityIndexManager.State newState) {
// Only consider applying migrations if it's the master node and the security index exists
if (clusterState.nodes().isLocalNodeElectedMaster() == false || newState.indexExists() == false) {
return;
}

// If no migrations have been applied and the security index is on the latest version (new index), all migrations can be skipped
if (newState.migrationsVersion == 0 && newState.createdOnLatestVersion) {
submitPersistentMigrationTask(SecurityMigrations.MIGRATIONS_BY_VERSION.lastKey(), false);
Expand All @@ -1235,7 +1237,9 @@ private void applyPendingSecurityMigrations(SecurityIndexManager.State newState)
);

// Check if next migration that has not been applied is eligible to run on the current cluster
if (nextMigration == null || systemIndices.getMainIndexManager().isEligibleSecurityMigration(nextMigration.getValue()) == false) {
if (nextMigration == null
|| systemIndices.getMainIndexManager().isEligibleSecurityMigration(nextMigration.getValue()) == false
|| nextMigration.getValue().checkPreconditions(clusterState) == false) {
// Reset retry counter if all eligible migrations have been applied successfully
nodeLocalMigrationRetryCount.set(0);
} else if (nodeLocalMigrationRetryCount.get() > MAX_SECURITY_MIGRATION_RETRY_COUNT) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package org.elasticsearch.xpack.security.action.rolemapping;

import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.metadata.ReservedStateMetadata;
import org.elasticsearch.reservedstate.ReservedClusterStateHandler;
import org.elasticsearch.reservedstate.TransformState;
import org.elasticsearch.xcontent.XContentParser;
Expand All @@ -34,6 +35,8 @@
*/
public class ReservedRoleMappingAction implements ReservedClusterStateHandler<List<PutRoleMappingRequest>> {
public static final String NAME = "role_mappings";
private static final String FILE_SETTINGS_METADATA_NAMESPACE = "file_settings";
private static final String HANDLER_ROLE_MAPPINGS_NAME = "role_mappings";

@Override
public String name() {
Expand Down Expand Up @@ -71,6 +74,11 @@ public List<PutRoleMappingRequest> fromXContent(XContentParser parser) throws IO
return result;
}

public static Set<String> getFileSettingsMetadataHandlerRoleMappingKeys(ClusterState clusterState) {
ReservedStateMetadata fileSettingsMetadata = clusterState.metadata().reservedStateMetadata().get(FILE_SETTINGS_METADATA_NAMESPACE);
return fileSettingsMetadata.handlers().get(HANDLER_ROLE_MAPPINGS_NAME).keys();
}

private Set<ExpressionRoleMapping> validate(List<PutRoleMappingRequest> roleMappings) {
var exceptions = new ArrayList<Exception>();
for (var roleMapping : roleMappings) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.threadpool.Scheduler;
import org.elasticsearch.xcontent.XContentType;
import org.elasticsearch.xpack.core.security.authz.RoleMappingMetadata;
import org.elasticsearch.xpack.security.SecurityFeatures;

import java.time.Instant;
Expand Down Expand Up @@ -285,6 +286,7 @@ public void clusterChanged(ClusterChangedEvent event) {
Tuple<Boolean, Boolean> available = checkIndexAvailable(event.state());
final boolean indexAvailableForWrite = available.v1();
final boolean indexAvailableForSearch = available.v2();
final RoleMappingMetadata clusterStateRoleMappingMetadata = RoleMappingMetadata.getFromClusterState(event.state());
final boolean mappingIsUpToDate = indexMetadata == null || checkIndexMappingUpToDate(event.state());
final int migrationsVersion = getMigrationVersionFromIndexMetadata(indexMetadata);
final SystemIndexDescriptor.MappingsVersion minClusterMappingVersion = getMinSecurityIndexMappingVersion(event.state());
Expand Down Expand Up @@ -324,7 +326,8 @@ public void clusterChanged(ClusterChangedEvent event) {
indexUUID,
allSecurityFeatures.stream()
.filter(feature -> featureService.clusterHasFeature(event.state(), feature))
.collect(Collectors.toSet())
.collect(Collectors.toSet()),
clusterStateRoleMappingMetadata
);
this.state = newState;

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

public RoleMappingMetadata getClusterStateRoleMappingMetadata() {
return state.clusterStateRoleMappingMetadata;
}

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 @@ -714,7 +721,8 @@ public static class State {
null,
null,
null,
Set.of()
Set.of(),
null
);
public final Instant creationTime;
public final boolean isIndexUpToDate;
Expand All @@ -732,6 +740,7 @@ public static class State {
public final IndexMetadata.State indexState;
public final String indexUUID;
public final Set<NodeFeature> securityFeatures;
public final RoleMappingMetadata clusterStateRoleMappingMetadata;

public State(
Instant creationTime,
Expand All @@ -747,7 +756,8 @@ public State(
ClusterHealthStatus indexHealth,
IndexMetadata.State indexState,
String indexUUID,
Set<NodeFeature> securityFeatures
Set<NodeFeature> securityFeatures,
RoleMappingMetadata clusterStateRoleMappingMetadata
) {
this.creationTime = creationTime;
this.isIndexUpToDate = isIndexUpToDate;
Expand All @@ -763,6 +773,7 @@ public State(
this.indexState = indexState;
this.indexUUID = indexUUID;
this.securityFeatures = securityFeatures;
this.clusterStateRoleMappingMetadata = clusterStateRoleMappingMetadata;
}

@Override
Expand All @@ -782,7 +793,8 @@ public boolean equals(Object o) {
&& Objects.equals(concreteIndexName, state.concreteIndexName)
&& indexHealth == state.indexHealth
&& indexState == state.indexState
&& Objects.equals(securityFeatures, state.securityFeatures);
&& Objects.equals(securityFeatures, state.securityFeatures)
&& Objects.equals(clusterStateRoleMappingMetadata, state.clusterStateRoleMappingMetadata);
}

public boolean indexExists() {
Expand All @@ -803,7 +815,8 @@ public int hashCode() {
indexMappingVersion,
concreteIndexName,
indexHealth,
securityFeatures
securityFeatures,
clusterStateRoleMappingMetadata
);
}

Expand Down Expand Up @@ -840,6 +853,8 @@ public String toString() {
+ '\''
+ ", securityFeatures="
+ securityFeatures
+ ", clusterStateRoleMappingMetadata="
+ clusterStateRoleMappingMetadata
+ '}';
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
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.cluster.ClusterState;
import org.elasticsearch.features.NodeFeature;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
Expand All @@ -20,12 +22,20 @@
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.xpack.core.security.action.rolemapping.DeleteRoleMappingRequestBuilder;
import org.elasticsearch.xpack.core.security.action.rolemapping.GetRoleMappingsRequestBuilder;
import org.elasticsearch.xpack.core.security.authc.support.mapper.ExpressionRoleMapping;
import org.elasticsearch.xpack.core.security.authz.RoleMappingMetadata;

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.xpack.security.action.rolemapping.ReservedRoleMappingAction.getFileSettingsMetadataHandlerRoleMappingKeys;
import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SecurityMainIndexMappingVersion.ADD_REMOTE_CLUSTER_AND_DESCRIPTION_FIELDS;

/**
Expand All @@ -38,11 +48,22 @@ public interface SecurityMigration {
/**
* Method that will execute the actual migration - needs to be idempotent and non-blocking
*
* @param indexManager for the security index
* @param securityIndexManager manager for the main security index
* @param client the index client
* @param listener listener to provide updates back to caller
*/
void migrate(SecurityIndexManager indexManager, Client client, ActionListener<Void> listener);
void migrate(SecurityIndexManager securityIndexManager, Client client, ActionListener<Void> listener);

/**
* Check preconditions to make sure the cluster is ready for the migration
*
* @param clusterState state of the cluster
*
* @return true if preconditions are met, false otherwise
*/
default boolean checkPreconditions(ClusterState clusterState) {
return true;
}

/**
* Any node features that are required for this migration to run. This makes sure that all nodes in the cluster can handle any
Expand All @@ -62,37 +83,33 @@ public interface SecurityMigration {
}

public static final Integer ROLE_METADATA_FLATTENED_MIGRATION_VERSION = 1;
public static final Integer OPERATOR_DEFINED_ROLE_MAPPINGS_CLEANUP = 2;

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) {
public void migrate(SecurityIndexManager securityIndexManager, Client client, ActionListener<Void> listener) {
BoolQueryBuilder filterQuery = new BoolQueryBuilder().filter(QueryBuilders.termQuery("type", "role"))
.mustNot(QueryBuilders.existsQuery("metadata_flattened"));
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(filterQuery).size(0).trackTotalHits(true);
SearchRequest countRequest = new SearchRequest(indexManager.getConcreteIndexName());
SearchRequest countRequest = new SearchRequest(securityIndexManager.getConcreteIndexName());
countRequest.source(searchSourceBuilder);

client.search(countRequest, ActionListener.wrap(response -> {
// If there are no roles, skip migration
if (response.getHits().getTotalHits().value > 0) {
logger.info("Preparing to migrate [" + response.getHits().getTotalHits().value + "] roles");
updateRolesByQuery(indexManager, client, filterQuery, listener);
updateRolesByQuery(securityIndexManager.getConcreteIndexName(), client, filterQuery, listener);
} else {
listener.onResponse(null);
}
}, listener::onFailure));
}

private void updateRolesByQuery(
SecurityIndexManager indexManager,
Client client,
BoolQueryBuilder filterQuery,
ActionListener<Void> listener
) {
UpdateByQueryRequest updateByQueryRequest = new UpdateByQueryRequest(indexManager.getConcreteIndexName());
private void updateRolesByQuery(String indexName, Client client, BoolQueryBuilder filterQuery, ActionListener<Void> listener) {
UpdateByQueryRequest updateByQueryRequest = new UpdateByQueryRequest(indexName);
updateByQueryRequest.setQuery(filterQuery);
updateByQueryRequest.setScript(
new Script(
Expand All @@ -115,6 +132,89 @@ public Set<NodeFeature> nodeFeaturesRequired() {
return Set.of(SecuritySystemIndices.SECURITY_ROLES_METADATA_FLATTENED);
}

@Override
public int minMappingVersion() {
return ADD_REMOTE_CLUSTER_AND_DESCRIPTION_FIELDS.id();
}
}, OPERATOR_DEFINED_ROLE_MAPPINGS_CLEANUP, new SecurityMigration() {
private static final Logger logger = LogManager.getLogger(SecurityMigration.class);

@Override
public void migrate(SecurityIndexManager securityIndexManager, Client client, ActionListener<Void> listener) {
if (securityIndexManager.getClusterStateRoleMappingMetadata().getRoleMappings().isEmpty()) {
listener.onResponse(null);
}

getNativeRoleMappingsToDelete(
client,
securityIndexManager.getClusterStateRoleMappingMetadata()
.getRoleMappings()
.stream()
.map(ExpressionRoleMapping::getName)
.toList(),
ActionListener.wrap(
roleMappingIds -> deleteNativeRoleMappings(client, roleMappingIds.iterator(), listener),
listener::onFailure
)
);
}

private void getNativeRoleMappingsToDelete(
Client client,
List<String> clusterStateRoleMappingNames,
ActionListener<List<String>> listener
) {
if (clusterStateRoleMappingNames.isEmpty()) {
listener.onResponse(List.of());
return;
}
getNativeRoleMappingFromNames(client, listener, clusterStateRoleMappingNames.toArray(String[]::new));
}

private void getNativeRoleMappingFromNames(Client client, ActionListener<List<String>> listener, String... names) {
new GetRoleMappingsRequestBuilder(client).names(names).execute(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();
new DeleteRoleMappingRequestBuilder(client).name(name)
.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE)
.execute(ActionListener.wrap(response -> {
if (response.isFound() == false) {
logger.warn("Expected role mapping [" + name + "] not found during role mapping clean up.");
}
if (namesIterator.hasNext()) {
deleteNativeRoleMappings(client, namesIterator, listener);
} else {
listener.onResponse(null);
}
}, listener::onFailure));
}

@Override
public boolean checkPreconditions(ClusterState clusterState) {
if (getFileSettingsMetadataHandlerRoleMappingKeys(clusterState).isEmpty()) {
// No operator defined role mappings, so doesn't need to be ready since cleanup is a noop
return true;
}

// TODO Change this to check version of role mapping metadata when available. So, if version is up to date -> needs upgrade
if (RoleMappingMetadata.getFromClusterState(clusterState).getRoleMappings().stream().anyMatch((roleMapping) -> true)) {
// Version is up-to-date for role mapping so ready for cleanup
return true;
}

// Version is not up-to-date, can't trigger cleanup
return false;
}

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

@Override
public int minMappingVersion() {
return ADD_REMOTE_CLUSTER_AND_DESCRIPTION_FIELDS.id();
Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -2522,7 +2522,8 @@ private SecurityIndexManager.State dummyState(ClusterHealthStatus indexStatus) {
indexStatus,
IndexMetadata.State.OPEN,
"my_uuid",
Set.of()
Set.of(),
null
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ private SecurityIndexManager.State dummyState(ClusterHealthStatus indexStatus) {
indexStatus,
IndexMetadata.State.OPEN,
"my_uuid",
Set.of()
Set.of(),
null
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,8 @@ private SecurityIndexManager.State indexState(boolean isUpToDate, ClusterHealthS
healthStatus,
IndexMetadata.State.OPEN,
"my_uuid",
Set.of()
Set.of(),
null
);
}

Expand Down
Loading