Skip to content

Commit

Permalink
Merge branch 'main' into adding-data-stream-index-deprecation
Browse files Browse the repository at this point in the history
  • Loading branch information
masseyke authored Nov 8, 2024
2 parents 40c5145 + af99654 commit 82feeac
Show file tree
Hide file tree
Showing 34 changed files with 1,157 additions and 205 deletions.
6 changes: 6 additions & 0 deletions docs/changelog/114964.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 114964
summary: Add a `monitor_stats` privilege and allow that privilege for remote cluster
privileges
area: Authorization
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ The result would then have the `errors` field set to `true` and hold the error f
"details": {
"my_admin_role": { <4>
"type": "action_request_validation_exception",
"reason": "Validation Failed: 1: unknown cluster privilege [bad_cluster_privilege]. a privilege must be either one of the predefined cluster privilege names [manage_own_api_key,manage_data_stream_global_retention,monitor_data_stream_global_retention,none,cancel_task,cross_cluster_replication,cross_cluster_search,delegate_pki,grant_api_key,manage_autoscaling,manage_index_templates,manage_logstash_pipelines,manage_oidc,manage_saml,manage_search_application,manage_search_query_rules,manage_search_synonyms,manage_service_account,manage_token,manage_user_profile,monitor_connector,monitor_enrich,monitor_inference,monitor_ml,monitor_rollup,monitor_snapshot,monitor_text_structure,monitor_watcher,post_behavioral_analytics_event,read_ccr,read_connector_secrets,read_fleet_secrets,read_ilm,read_pipeline,read_security,read_slm,transport_client,write_connector_secrets,write_fleet_secrets,create_snapshot,manage_behavioral_analytics,manage_ccr,manage_connector,manage_enrich,manage_ilm,manage_inference,manage_ml,manage_rollup,manage_slm,manage_watcher,monitor_data_frame_transforms,monitor_transform,manage_api_key,manage_ingest_pipelines,manage_pipeline,manage_data_frame_transforms,manage_transform,manage_security,monitor,manage,all] or a pattern over one of the available cluster actions;"
"reason": "Validation Failed: 1: unknown cluster privilege [bad_cluster_privilege]. a privilege must be either one of the predefined cluster privilege names [manage_own_api_key,manage_data_stream_global_retention,monitor_data_stream_global_retention,none,cancel_task,cross_cluster_replication,cross_cluster_search,delegate_pki,grant_api_key,manage_autoscaling,manage_index_templates,manage_logstash_pipelines,manage_oidc,manage_saml,manage_search_application,manage_search_query_rules,manage_search_synonyms,manage_service_account,manage_token,manage_user_profile,monitor_connector,monitor_enrich,monitor_inference,monitor_ml,monitor_rollup,monitor_snapshot,monitor_stats,monitor_text_structure,monitor_watcher,post_behavioral_analytics_event,read_ccr,read_connector_secrets,read_fleet_secrets,read_ilm,read_pipeline,read_security,read_slm,transport_client,write_connector_secrets,write_fleet_secrets,create_snapshot,manage_behavioral_analytics,manage_ccr,manage_connector,manage_enrich,manage_ilm,manage_inference,manage_ml,manage_rollup,manage_slm,manage_watcher,monitor_data_frame_transforms,monitor_transform,manage_api_key,manage_ingest_pipelines,manage_pipeline,manage_data_frame_transforms,manage_transform,manage_security,monitor,manage,all] or a pattern over one of the available cluster actions;"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ A successful call returns an object with "cluster", "index", and "remote_cluster
"monitor_ml",
"monitor_rollup",
"monitor_snapshot",
"monitor_stats",
"monitor_text_structure",
"monitor_transform",
"monitor_watcher",
Expand Down Expand Up @@ -152,7 +153,8 @@ A successful call returns an object with "cluster", "index", and "remote_cluster
"write"
],
"remote_cluster" : [
"monitor_enrich"
"monitor_enrich",
"monitor_stats"
]
}
--------------------------------------------------
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ static TransportVersion def(int id) {
public static final TransportVersion LOGSDB_TELEMETRY = def(8_784_00_0);
public static final TransportVersion LOGSDB_TELEMETRY_STATS = def(8_785_00_0);
public static final TransportVersion KQL_QUERY_ADDED = def(8_786_00_0);
public static final TransportVersion DATA_STREAM_INDEX_VERSION_DEPRECATION_CHECK = def(8_787_00_0);
public static final TransportVersion ROLE_MONITOR_STATS = def(8_787_00_0);
public static final TransportVersion DATA_STREAM_INDEX_VERSION_DEPRECATION_CHECK = def(8_788_00_0);

/*
* STOP! READ THIS FIRST! No, really,
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,6 @@ tasks.named("yamlRestCompatTestTransform").configure({ task ->
task.skipTest("esql/60_usage/Basic ESQL usage output (telemetry) non-snapshot version", "The number of functions is constantly increasing")
task.skipTest("esql/80_text/reverse text", "The output type changed from TEXT to KEYWORD.")
task.skipTest("esql/80_text/values function", "The output type changed from TEXT to KEYWORD.")
task.skipTest("privileges/11_builtin/Test get builtin privileges" ,"unnecessary to test compatibility")
})

Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public boolean hasRemoteIndicesPrivileges() {
}

public boolean hasRemoteClusterPrivileges() {
return remoteClusterPermissions.hasPrivileges();
return remoteClusterPermissions.hasAnyPrivileges();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.permission.RemoteClusterPermissions;
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
import org.elasticsearch.xpack.core.security.user.InternalUser;
import org.elasticsearch.xpack.core.security.user.InternalUsers;
Expand Down Expand Up @@ -76,6 +77,7 @@
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.FALLBACK_REALM_NAME;
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.FALLBACK_REALM_TYPE;
import static org.elasticsearch.xpack.core.security.authc.RealmDomain.REALM_DOMAIN_PARSER;
import static org.elasticsearch.xpack.core.security.authz.RoleDescriptor.Fields.REMOTE_CLUSTER;
import static org.elasticsearch.xpack.core.security.authz.permission.RemoteClusterPermissions.ROLE_REMOTE_CLUSTER_PRIVS;

/**
Expand Down Expand Up @@ -233,8 +235,8 @@ public Authentication maybeRewriteForOlderVersion(TransportVersion olderVersion)
+ "]"
);
}

final Map<String, Object> newMetadata = maybeRewriteMetadata(olderVersion, this);

final Authentication newAuthentication;
if (isRunAs()) {
// The lookup user for run-as currently doesn't have authentication metadata associated with them because
Expand Down Expand Up @@ -272,12 +274,23 @@ public Authentication maybeRewriteForOlderVersion(TransportVersion olderVersion)
}

private static Map<String, Object> maybeRewriteMetadata(TransportVersion olderVersion, Authentication authentication) {
if (authentication.isAuthenticatedAsApiKey()) {
return maybeRewriteMetadataForApiKeyRoleDescriptors(olderVersion, authentication);
} else if (authentication.isCrossClusterAccess()) {
return maybeRewriteMetadataForCrossClusterAccessAuthentication(olderVersion, authentication);
} else {
return authentication.getAuthenticatingSubject().getMetadata();
try {
if (authentication.isAuthenticatedAsApiKey()) {
return maybeRewriteMetadataForApiKeyRoleDescriptors(olderVersion, authentication);
} else if (authentication.isCrossClusterAccess()) {
return maybeRewriteMetadataForCrossClusterAccessAuthentication(olderVersion, authentication);
} else {
return authentication.getAuthenticatingSubject().getMetadata();
}
} catch (Exception e) {
// CCS workflows may swallow the exception message making this difficult to troubleshoot, so we explicitly log and re-throw
// here. It may result in duplicate logs, so we only log the message at warn level.
if (logger.isDebugEnabled()) {
logger.debug("Un-expected exception thrown while rewriting metadata. This is likely a bug.", e);
} else {
logger.warn("Un-expected exception thrown while rewriting metadata. This is likely a bug [" + e.getMessage() + "]");
}
throw e;
}
}

Expand Down Expand Up @@ -1323,6 +1336,7 @@ private static Map<String, Object> maybeRewriteMetadataForApiKeyRoleDescriptors(

if (authentication.getEffectiveSubject().getTransportVersion().onOrAfter(ROLE_REMOTE_CLUSTER_PRIVS)
&& streamVersion.before(ROLE_REMOTE_CLUSTER_PRIVS)) {
// the authentication understands the remote_cluster field but the stream does not
metadata = new HashMap<>(metadata);
metadata.put(
AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY,
Expand All @@ -1336,7 +1350,26 @@ private static Map<String, Object> maybeRewriteMetadataForApiKeyRoleDescriptors(
(BytesReference) metadata.get(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY)
)
);
}
} else if (authentication.getEffectiveSubject().getTransportVersion().onOrAfter(ROLE_REMOTE_CLUSTER_PRIVS)
&& streamVersion.onOrAfter(ROLE_REMOTE_CLUSTER_PRIVS)) {
// both the authentication object and the stream understand the remote_cluster field
// check each individual permission and remove as needed
metadata = new HashMap<>(metadata);
metadata.put(
AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY,
maybeRemoveRemoteClusterPrivilegesFromRoleDescriptors(
(BytesReference) metadata.get(AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY),
streamVersion
)
);
metadata.put(
AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY,
maybeRemoveRemoteClusterPrivilegesFromRoleDescriptors(
(BytesReference) metadata.get(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY),
streamVersion
)
);
}

if (authentication.getEffectiveSubject().getTransportVersion().onOrAfter(VERSION_API_KEY_ROLES_AS_BYTES)
&& streamVersion.before(VERSION_API_KEY_ROLES_AS_BYTES)) {
Expand Down Expand Up @@ -1417,7 +1450,7 @@ private static BytesReference convertRoleDescriptorsMapToBytes(Map<String, Objec
}

static BytesReference maybeRemoveRemoteClusterFromRoleDescriptors(BytesReference roleDescriptorsBytes) {
return maybeRemoveTopLevelFromRoleDescriptors(roleDescriptorsBytes, RoleDescriptor.Fields.REMOTE_CLUSTER.getPreferredName());
return maybeRemoveTopLevelFromRoleDescriptors(roleDescriptorsBytes, REMOTE_CLUSTER.getPreferredName());
}

static BytesReference maybeRemoveRemoteIndicesFromRoleDescriptors(BytesReference roleDescriptorsBytes) {
Expand Down Expand Up @@ -1450,6 +1483,66 @@ static BytesReference maybeRemoveTopLevelFromRoleDescriptors(BytesReference role
}
}

/**
* Before we send the role descriptors to the remote cluster, we need to remove the remote cluster privileges that the other cluster
* will not understand. If all privileges are removed, then the entire "remote_cluster" is removed to avoid sending empty privileges.
* @param roleDescriptorsBytes The role descriptors to be sent to the remote cluster, represented as bytes.
* @return The role descriptors with the privileges that unsupported by version removed, represented as bytes.
*/
@SuppressWarnings("unchecked")
static BytesReference maybeRemoveRemoteClusterPrivilegesFromRoleDescriptors(
BytesReference roleDescriptorsBytes,
TransportVersion outboundVersion
) {
if (roleDescriptorsBytes == null || roleDescriptorsBytes.length() == 0) {
return roleDescriptorsBytes;
}
final Map<String, Object> roleDescriptorsMap = convertRoleDescriptorsBytesToMap(roleDescriptorsBytes);
final Map<String, Object> roleDescriptorsMapMutated = new HashMap<>(roleDescriptorsMap);
final AtomicBoolean modified = new AtomicBoolean(false);
roleDescriptorsMap.forEach((key, value) -> {
if (value instanceof Map) {
Map<String, Object> roleDescriptor = (Map<String, Object>) value;
roleDescriptor.forEach((innerKey, innerValue) -> {
// example: remote_cluster=[{privileges=[monitor_enrich, monitor_stats]
if (REMOTE_CLUSTER.getPreferredName().equals(innerKey)) {
assert innerValue instanceof List;
RemoteClusterPermissions discoveredRemoteClusterPermission = new RemoteClusterPermissions(
(List<Map<String, List<String>>>) innerValue
);
RemoteClusterPermissions mutated = discoveredRemoteClusterPermission.removeUnsupportedPrivileges(outboundVersion);
if (mutated.equals(discoveredRemoteClusterPermission) == false) {
// swap out the old value with the new value
modified.set(true);
Map<String, Object> remoteClusterMap = new HashMap<>((Map<String, Object>) roleDescriptorsMapMutated.get(key));
if (mutated.hasAnyPrivileges()) {
// has at least one group with privileges
remoteClusterMap.put(innerKey, mutated.toMap());
} else {
// has no groups with privileges
remoteClusterMap.remove(innerKey);
}
roleDescriptorsMapMutated.put(key, remoteClusterMap);
}
}
});
}
});
if (modified.get()) {
logger.debug(
"mutated role descriptors. Changed from {} to {} for outbound version {}",
roleDescriptorsMap,
roleDescriptorsMapMutated,
outboundVersion
);
return convertRoleDescriptorsMapToBytes(roleDescriptorsMapMutated);
} else {
// No need to serialize if we did not change anything.
logger.trace("no change to role descriptors {} for outbound version {}", roleDescriptorsMap, outboundVersion);
return roleDescriptorsBytes;
}
}

static boolean equivalentRealms(String name1, String type1, String name2, String type2) {
if (false == type1.equals(type2)) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
*/
package org.elasticsearch.xpack.core.security.authz;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.TransportVersion;
Expand Down Expand Up @@ -62,6 +64,7 @@ public class RoleDescriptor implements ToXContentObject, Writeable {
public static final TransportVersion SECURITY_ROLE_DESCRIPTION = TransportVersions.V_8_15_0;

public static final String ROLE_TYPE = "role";
private static final Logger logger = LogManager.getLogger(RoleDescriptor.class);

private final String name;
private final String[] clusterPrivileges;
Expand Down Expand Up @@ -191,7 +194,7 @@ public RoleDescriptor(
? Collections.unmodifiableMap(transientMetadata)
: Collections.singletonMap("enabled", true);
this.remoteIndicesPrivileges = remoteIndicesPrivileges != null ? remoteIndicesPrivileges : RemoteIndicesPrivileges.NONE;
this.remoteClusterPermissions = remoteClusterPermissions != null && remoteClusterPermissions.hasPrivileges()
this.remoteClusterPermissions = remoteClusterPermissions != null && remoteClusterPermissions.hasAnyPrivileges()
? remoteClusterPermissions
: RemoteClusterPermissions.NONE;
this.restriction = restriction != null ? restriction : Restriction.NONE;
Expand Down Expand Up @@ -263,7 +266,7 @@ public boolean hasRemoteIndicesPrivileges() {
}

public boolean hasRemoteClusterPermissions() {
return remoteClusterPermissions.hasPrivileges();
return remoteClusterPermissions.hasAnyPrivileges();
}

public RemoteClusterPermissions getRemoteClusterPermissions() {
Expand Down Expand Up @@ -830,25 +833,32 @@ private static RemoteClusterPermissions parseRemoteCluster(final String roleName
currentFieldName = parser.currentName();
} else if (Fields.PRIVILEGES.match(currentFieldName, parser.getDeprecationHandler())) {
privileges = readStringArray(roleName, parser, false);
if (privileges.length != 1
|| RemoteClusterPermissions.getSupportedRemoteClusterPermissions()
.contains(privileges[0].trim().toLowerCase(Locale.ROOT)) == false) {
throw new ElasticsearchParseException(
"failed to parse remote_cluster for role [{}]. "
+ RemoteClusterPermissions.getSupportedRemoteClusterPermissions()
+ " is the only value allowed for [{}] within [remote_cluster]",
if (Arrays.stream(privileges)
.map(s -> s.toLowerCase(Locale.ROOT).trim())
.allMatch(RemoteClusterPermissions.getSupportedRemoteClusterPermissions()::contains) == false) {
final String message = String.format(
Locale.ROOT,
"failed to parse remote_cluster for role [%s]. "
+ "%s are the only values allowed for [%s] within [remote_cluster]. Found %s",
roleName,
currentFieldName
RemoteClusterPermissions.getSupportedRemoteClusterPermissions(),
currentFieldName,
Arrays.toString(privileges)
);
logger.info(message);
throw new ElasticsearchParseException(message);
}
} else if (Fields.CLUSTERS.match(currentFieldName, parser.getDeprecationHandler())) {
clusters = readStringArray(roleName, parser, false);
} else {
throw new ElasticsearchParseException(
"failed to parse remote_cluster for role [{}]. unexpected field [{}]",
final String message = String.format(
Locale.ROOT,
"failed to parse remote_cluster for role [%s]. unexpected field [%s]",
roleName,
currentFieldName
);
logger.info(message);
throw new ElasticsearchParseException(message);
}
}
if (privileges != null && clusters == null) {
Expand Down
Loading

0 comments on commit 82feeac

Please sign in to comment.