Skip to content

Commit f93ffc4

Browse files
committed
Add a monitor_stats privilege and allow that privilege for remote cluster privileges (#114964)
This commit does the following: * Add a new monitor_stats privilege * Ensure that monitor_stats can be set in the remote_cluster privileges * Give's Kibana the ability to remotely call monitor_stats via RCS 2.0 Since this is the first case where there is more than 1 remote_cluster privilege, the following framework concern has been added: * Ensure that when sending to elder RCS 2.0 clusters that we don't send the new privilege previous only supported all or nothing remote_cluster blocks * Ensure that we when sending API key role descriptors that contains remote_cluster, we don't send the new privileges for RCS 1.0/2.0 if it not new enough * Fix and extend the BWC tests for RCS 1.0 and RCS 2.0 (cherry picked from commit af99654)
1 parent 47be7e4 commit f93ffc4

File tree

34 files changed

+1156
-204
lines changed

34 files changed

+1156
-204
lines changed

docs/changelog/114964.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 114964
2+
summary: Add a `monitor_stats` privilege and allow that privilege for remote cluster
3+
privileges
4+
area: Authorization
5+
type: enhancement
6+
issues: []

docs/reference/rest-api/security/bulk-create-roles.asciidoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ The result would then have the `errors` field set to `true` and hold the error f
327327
"details": {
328328
"my_admin_role": { <4>
329329
"type": "action_request_validation_exception",
330-
"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;"
330+
"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;"
331331
}
332332
}
333333
}

docs/reference/rest-api/security/get-builtin-privileges.asciidoc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ A successful call returns an object with "cluster", "index", and "remote_cluster
111111
"monitor_ml",
112112
"monitor_rollup",
113113
"monitor_snapshot",
114+
"monitor_stats",
114115
"monitor_text_structure",
115116
"monitor_transform",
116117
"monitor_watcher",
@@ -152,7 +153,8 @@ A successful call returns an object with "cluster", "index", and "remote_cluster
152153
"write"
153154
],
154155
"remote_cluster" : [
155-
"monitor_enrich"
156+
"monitor_enrich",
157+
"monitor_stats"
156158
]
157159
}
158160
--------------------------------------------------

server/src/main/java/org/elasticsearch/TransportVersions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ static TransportVersion def(int id) {
189189
public static final TransportVersion LOGSDB_TELEMETRY = def(8_784_00_0);
190190
public static final TransportVersion LOGSDB_TELEMETRY_STATS = def(8_785_00_0);
191191
public static final TransportVersion KQL_QUERY_ADDED = def(8_786_00_0);
192+
public static final TransportVersion ROLE_MONITOR_STATS = def(8_787_00_0);
192193

193194
/*
194195
* STOP! READ THIS FIRST! No, really,

x-pack/plugin/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,5 +207,6 @@ tasks.named("yamlRestTestV7CompatTransform").configure({ task ->
207207
task.skipTest("esql/60_usage/Basic ESQL usage output (telemetry) non-snapshot version", "The number of functions is constantly increasing")
208208
task.skipTest("esql/80_text/reverse text", "The output type changed from TEXT to KEYWORD.")
209209
task.skipTest("esql/80_text/values function", "The output type changed from TEXT to KEYWORD.")
210+
task.skipTest("privileges/11_builtin/Test get builtin privileges" ,"unnecessary to test compatibility")
210211
})
211212

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesResponse.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public boolean hasRemoteIndicesPrivileges() {
115115
}
116116

117117
public boolean hasRemoteClusterPrivileges() {
118-
return remoteClusterPermissions.hasPrivileges();
118+
return remoteClusterPermissions.hasAnyPrivileges();
119119
}
120120

121121
@Override

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
3737
import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer;
3838
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
39+
import org.elasticsearch.xpack.core.security.authz.permission.RemoteClusterPermissions;
3940
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
4041
import org.elasticsearch.xpack.core.security.user.InternalUser;
4142
import org.elasticsearch.xpack.core.security.user.InternalUsers;
@@ -76,6 +77,7 @@
7677
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.FALLBACK_REALM_NAME;
7778
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.FALLBACK_REALM_TYPE;
7879
import static org.elasticsearch.xpack.core.security.authc.RealmDomain.REALM_DOMAIN_PARSER;
80+
import static org.elasticsearch.xpack.core.security.authz.RoleDescriptor.Fields.REMOTE_CLUSTER;
7981
import static org.elasticsearch.xpack.core.security.authz.permission.RemoteClusterPermissions.ROLE_REMOTE_CLUSTER_PRIVS;
8082

8183
/**
@@ -233,8 +235,8 @@ public Authentication maybeRewriteForOlderVersion(TransportVersion olderVersion)
233235
+ "]"
234236
);
235237
}
236-
237238
final Map<String, Object> newMetadata = maybeRewriteMetadata(olderVersion, this);
239+
238240
final Authentication newAuthentication;
239241
if (isRunAs()) {
240242
// The lookup user for run-as currently doesn't have authentication metadata associated with them because
@@ -272,12 +274,23 @@ public Authentication maybeRewriteForOlderVersion(TransportVersion olderVersion)
272274
}
273275

274276
private static Map<String, Object> maybeRewriteMetadata(TransportVersion olderVersion, Authentication authentication) {
275-
if (authentication.isAuthenticatedAsApiKey()) {
276-
return maybeRewriteMetadataForApiKeyRoleDescriptors(olderVersion, authentication);
277-
} else if (authentication.isCrossClusterAccess()) {
278-
return maybeRewriteMetadataForCrossClusterAccessAuthentication(olderVersion, authentication);
279-
} else {
280-
return authentication.getAuthenticatingSubject().getMetadata();
277+
try {
278+
if (authentication.isAuthenticatedAsApiKey()) {
279+
return maybeRewriteMetadataForApiKeyRoleDescriptors(olderVersion, authentication);
280+
} else if (authentication.isCrossClusterAccess()) {
281+
return maybeRewriteMetadataForCrossClusterAccessAuthentication(olderVersion, authentication);
282+
} else {
283+
return authentication.getAuthenticatingSubject().getMetadata();
284+
}
285+
} catch (Exception e) {
286+
// CCS workflows may swallow the exception message making this difficult to troubleshoot, so we explicitly log and re-throw
287+
// here. It may result in duplicate logs, so we only log the message at warn level.
288+
if (logger.isDebugEnabled()) {
289+
logger.debug("Un-expected exception thrown while rewriting metadata. This is likely a bug.", e);
290+
} else {
291+
logger.warn("Un-expected exception thrown while rewriting metadata. This is likely a bug [" + e.getMessage() + "]");
292+
}
293+
throw e;
281294
}
282295
}
283296

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

13241337
if (authentication.getEffectiveSubject().getTransportVersion().onOrAfter(ROLE_REMOTE_CLUSTER_PRIVS)
13251338
&& streamVersion.before(ROLE_REMOTE_CLUSTER_PRIVS)) {
1339+
// the authentication understands the remote_cluster field but the stream does not
13261340
metadata = new HashMap<>(metadata);
13271341
metadata.put(
13281342
AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY,
@@ -1336,7 +1350,26 @@ private static Map<String, Object> maybeRewriteMetadataForApiKeyRoleDescriptors(
13361350
(BytesReference) metadata.get(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY)
13371351
)
13381352
);
1339-
}
1353+
} else if (authentication.getEffectiveSubject().getTransportVersion().onOrAfter(ROLE_REMOTE_CLUSTER_PRIVS)
1354+
&& streamVersion.onOrAfter(ROLE_REMOTE_CLUSTER_PRIVS)) {
1355+
// both the authentication object and the stream understand the remote_cluster field
1356+
// check each individual permission and remove as needed
1357+
metadata = new HashMap<>(metadata);
1358+
metadata.put(
1359+
AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY,
1360+
maybeRemoveRemoteClusterPrivilegesFromRoleDescriptors(
1361+
(BytesReference) metadata.get(AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY),
1362+
streamVersion
1363+
)
1364+
);
1365+
metadata.put(
1366+
AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY,
1367+
maybeRemoveRemoteClusterPrivilegesFromRoleDescriptors(
1368+
(BytesReference) metadata.get(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY),
1369+
streamVersion
1370+
)
1371+
);
1372+
}
13401373

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

14191452
static BytesReference maybeRemoveRemoteClusterFromRoleDescriptors(BytesReference roleDescriptorsBytes) {
1420-
return maybeRemoveTopLevelFromRoleDescriptors(roleDescriptorsBytes, RoleDescriptor.Fields.REMOTE_CLUSTER.getPreferredName());
1453+
return maybeRemoveTopLevelFromRoleDescriptors(roleDescriptorsBytes, REMOTE_CLUSTER.getPreferredName());
14211454
}
14221455

14231456
static BytesReference maybeRemoveRemoteIndicesFromRoleDescriptors(BytesReference roleDescriptorsBytes) {
@@ -1450,6 +1483,66 @@ static BytesReference maybeRemoveTopLevelFromRoleDescriptors(BytesReference role
14501483
}
14511484
}
14521485

1486+
/**
1487+
* Before we send the role descriptors to the remote cluster, we need to remove the remote cluster privileges that the other cluster
1488+
* will not understand. If all privileges are removed, then the entire "remote_cluster" is removed to avoid sending empty privileges.
1489+
* @param roleDescriptorsBytes The role descriptors to be sent to the remote cluster, represented as bytes.
1490+
* @return The role descriptors with the privileges that unsupported by version removed, represented as bytes.
1491+
*/
1492+
@SuppressWarnings("unchecked")
1493+
static BytesReference maybeRemoveRemoteClusterPrivilegesFromRoleDescriptors(
1494+
BytesReference roleDescriptorsBytes,
1495+
TransportVersion outboundVersion
1496+
) {
1497+
if (roleDescriptorsBytes == null || roleDescriptorsBytes.length() == 0) {
1498+
return roleDescriptorsBytes;
1499+
}
1500+
final Map<String, Object> roleDescriptorsMap = convertRoleDescriptorsBytesToMap(roleDescriptorsBytes);
1501+
final Map<String, Object> roleDescriptorsMapMutated = new HashMap<>(roleDescriptorsMap);
1502+
final AtomicBoolean modified = new AtomicBoolean(false);
1503+
roleDescriptorsMap.forEach((key, value) -> {
1504+
if (value instanceof Map) {
1505+
Map<String, Object> roleDescriptor = (Map<String, Object>) value;
1506+
roleDescriptor.forEach((innerKey, innerValue) -> {
1507+
// example: remote_cluster=[{privileges=[monitor_enrich, monitor_stats]
1508+
if (REMOTE_CLUSTER.getPreferredName().equals(innerKey)) {
1509+
assert innerValue instanceof List;
1510+
RemoteClusterPermissions discoveredRemoteClusterPermission = new RemoteClusterPermissions(
1511+
(List<Map<String, List<String>>>) innerValue
1512+
);
1513+
RemoteClusterPermissions mutated = discoveredRemoteClusterPermission.removeUnsupportedPrivileges(outboundVersion);
1514+
if (mutated.equals(discoveredRemoteClusterPermission) == false) {
1515+
// swap out the old value with the new value
1516+
modified.set(true);
1517+
Map<String, Object> remoteClusterMap = new HashMap<>((Map<String, Object>) roleDescriptorsMapMutated.get(key));
1518+
if (mutated.hasAnyPrivileges()) {
1519+
// has at least one group with privileges
1520+
remoteClusterMap.put(innerKey, mutated.toMap());
1521+
} else {
1522+
// has no groups with privileges
1523+
remoteClusterMap.remove(innerKey);
1524+
}
1525+
roleDescriptorsMapMutated.put(key, remoteClusterMap);
1526+
}
1527+
}
1528+
});
1529+
}
1530+
});
1531+
if (modified.get()) {
1532+
logger.debug(
1533+
"mutated role descriptors. Changed from {} to {} for outbound version {}",
1534+
roleDescriptorsMap,
1535+
roleDescriptorsMapMutated,
1536+
outboundVersion
1537+
);
1538+
return convertRoleDescriptorsMapToBytes(roleDescriptorsMapMutated);
1539+
} else {
1540+
// No need to serialize if we did not change anything.
1541+
logger.trace("no change to role descriptors {} for outbound version {}", roleDescriptorsMap, outboundVersion);
1542+
return roleDescriptorsBytes;
1543+
}
1544+
}
1545+
14531546
static boolean equivalentRealms(String name1, String type1, String name2, String type2) {
14541547
if (false == type1.equals(type2)) {
14551548
return false;

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

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
*/
77
package org.elasticsearch.xpack.core.security.authz;
88

9+
import org.apache.logging.log4j.LogManager;
10+
import org.apache.logging.log4j.Logger;
911
import org.elasticsearch.ElasticsearchParseException;
1012
import org.elasticsearch.ElasticsearchSecurityException;
1113
import org.elasticsearch.TransportVersion;
@@ -62,6 +64,7 @@ public class RoleDescriptor implements ToXContentObject, Writeable {
6264
public static final TransportVersion SECURITY_ROLE_DESCRIPTION = TransportVersions.V_8_15_0;
6365

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

6669
private final String name;
6770
private final String[] clusterPrivileges;
@@ -191,7 +194,7 @@ public RoleDescriptor(
191194
? Collections.unmodifiableMap(transientMetadata)
192195
: Collections.singletonMap("enabled", true);
193196
this.remoteIndicesPrivileges = remoteIndicesPrivileges != null ? remoteIndicesPrivileges : RemoteIndicesPrivileges.NONE;
194-
this.remoteClusterPermissions = remoteClusterPermissions != null && remoteClusterPermissions.hasPrivileges()
197+
this.remoteClusterPermissions = remoteClusterPermissions != null && remoteClusterPermissions.hasAnyPrivileges()
195198
? remoteClusterPermissions
196199
: RemoteClusterPermissions.NONE;
197200
this.restriction = restriction != null ? restriction : Restriction.NONE;
@@ -263,7 +266,7 @@ public boolean hasRemoteIndicesPrivileges() {
263266
}
264267

265268
public boolean hasRemoteClusterPermissions() {
266-
return remoteClusterPermissions.hasPrivileges();
269+
return remoteClusterPermissions.hasAnyPrivileges();
267270
}
268271

269272
public RemoteClusterPermissions getRemoteClusterPermissions() {
@@ -830,25 +833,32 @@ private static RemoteClusterPermissions parseRemoteCluster(final String roleName
830833
currentFieldName = parser.currentName();
831834
} else if (Fields.PRIVILEGES.match(currentFieldName, parser.getDeprecationHandler())) {
832835
privileges = readStringArray(roleName, parser, false);
833-
if (privileges.length != 1
834-
|| RemoteClusterPermissions.getSupportedRemoteClusterPermissions()
835-
.contains(privileges[0].trim().toLowerCase(Locale.ROOT)) == false) {
836-
throw new ElasticsearchParseException(
837-
"failed to parse remote_cluster for role [{}]. "
838-
+ RemoteClusterPermissions.getSupportedRemoteClusterPermissions()
839-
+ " is the only value allowed for [{}] within [remote_cluster]",
836+
if (Arrays.stream(privileges)
837+
.map(s -> s.toLowerCase(Locale.ROOT).trim())
838+
.allMatch(RemoteClusterPermissions.getSupportedRemoteClusterPermissions()::contains) == false) {
839+
final String message = String.format(
840+
Locale.ROOT,
841+
"failed to parse remote_cluster for role [%s]. "
842+
+ "%s are the only values allowed for [%s] within [remote_cluster]. Found %s",
840843
roleName,
841-
currentFieldName
844+
RemoteClusterPermissions.getSupportedRemoteClusterPermissions(),
845+
currentFieldName,
846+
Arrays.toString(privileges)
842847
);
848+
logger.info(message);
849+
throw new ElasticsearchParseException(message);
843850
}
844851
} else if (Fields.CLUSTERS.match(currentFieldName, parser.getDeprecationHandler())) {
845852
clusters = readStringArray(roleName, parser, false);
846853
} else {
847-
throw new ElasticsearchParseException(
848-
"failed to parse remote_cluster for role [{}]. unexpected field [{}]",
854+
final String message = String.format(
855+
Locale.ROOT,
856+
"failed to parse remote_cluster for role [%s]. unexpected field [%s]",
849857
roleName,
850858
currentFieldName
851859
);
860+
logger.info(message);
861+
throw new ElasticsearchParseException(message);
852862
}
853863
}
854864
if (privileges != null && clusters == null) {

0 commit comments

Comments
 (0)