Skip to content

Commit cf80784

Browse files
Support API keys as authentication subjects for RCS 2.0 (#93414)
This PR enables support for defining remote_indices for API keys in order to be able to use them to authenticate cross cluster calls. The main change is made to the role classes (SimpleRole and LimitedRole), which now support constructing RoleDescriptorsIntersection on the querying cluster side. This RoleDescriptorsIntersection is sent to the fulfilling cluster as part of the RemoteAccessAuthentication. Relates to #90614
1 parent f5af004 commit cf80784

File tree

20 files changed

+1922
-254
lines changed

20 files changed

+1922
-254
lines changed

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

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings;
3333
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
3434
import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer;
35+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
3536
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
3637
import org.elasticsearch.xpack.core.security.user.AsyncSearchUser;
3738
import org.elasticsearch.xpack.core.security.user.SecurityProfileUser;
@@ -49,6 +50,7 @@
4950
import java.util.Locale;
5051
import java.util.Map;
5152
import java.util.Objects;
53+
import java.util.concurrent.atomic.AtomicBoolean;
5254

5355
import static org.elasticsearch.common.Strings.EMPTY_ARRAY;
5456
import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;
@@ -106,6 +108,7 @@ public final class Authentication implements ToXContentObject {
106108
public static final TransportVersion VERSION_API_KEY_ROLES_AS_BYTES = TransportVersion.V_7_9_0;
107109
public static final TransportVersion VERSION_REALM_DOMAINS = TransportVersion.V_8_2_0;
108110
public static final TransportVersion VERSION_METADATA_BEYOND_GENERIC_MAP = TransportVersion.V_8_8_0;
111+
public static final TransportVersion VERSION_API_KEYS_WITH_REMOTE_INDICES = TransportVersion.V_8_8_0;
109112
private final AuthenticationType type;
110113
private final Subject authenticatingSubject;
111114
private final Subject effectiveSubject;
@@ -1066,9 +1069,25 @@ private static Map<String, Object> maybeRewriteMetadataForApiKeyRoleDescriptors(
10661069
: "metadata must contain role descriptor for API key authentication";
10671070
assert metadata.containsKey(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY)
10681071
: "metadata must contain limited role descriptor for API key authentication";
1072+
if (authentication.getEffectiveSubject().getTransportVersion().onOrAfter(VERSION_API_KEYS_WITH_REMOTE_INDICES)
1073+
&& streamVersion.before(VERSION_API_KEYS_WITH_REMOTE_INDICES)) {
1074+
metadata = new HashMap<>(metadata);
1075+
metadata.put(
1076+
AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY,
1077+
maybeRemoveRemoteIndicesFromRoleDescriptors(
1078+
(BytesReference) metadata.get(AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY)
1079+
)
1080+
);
1081+
metadata.put(
1082+
AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY,
1083+
maybeRemoveRemoteIndicesFromRoleDescriptors(
1084+
(BytesReference) metadata.get(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY)
1085+
)
1086+
);
1087+
}
10691088
if (authentication.getEffectiveSubject().getTransportVersion().onOrAfter(VERSION_API_KEY_ROLES_AS_BYTES)
10701089
&& streamVersion.before(VERSION_API_KEY_ROLES_AS_BYTES)) {
1071-
metadata = new HashMap<>(metadata);
1090+
metadata = metadata instanceof HashMap ? metadata : new HashMap<>(metadata);
10721091
metadata.put(
10731092
AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY,
10741093
convertRoleDescriptorsBytesToMap((BytesReference) metadata.get(AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY))
@@ -1131,6 +1150,32 @@ private static BytesReference convertRoleDescriptorsMapToBytes(Map<String, Objec
11311150
}
11321151
}
11331152

1153+
static BytesReference maybeRemoveRemoteIndicesFromRoleDescriptors(BytesReference roleDescriptorsBytes) {
1154+
if (roleDescriptorsBytes == null || roleDescriptorsBytes.length() == 0) {
1155+
return roleDescriptorsBytes;
1156+
}
1157+
1158+
final Map<String, Object> roleDescriptorsMap = convertRoleDescriptorsBytesToMap(roleDescriptorsBytes);
1159+
final AtomicBoolean removedAtLeastOne = new AtomicBoolean(false);
1160+
roleDescriptorsMap.entrySet().stream().forEach(entry -> {
1161+
if (entry.getValue() instanceof Map) {
1162+
@SuppressWarnings("unchecked")
1163+
Map<String, Object> roleDescriptor = (Map<String, Object>) entry.getValue();
1164+
boolean removed = roleDescriptor.remove(RoleDescriptor.Fields.REMOTE_INDICES.getPreferredName()) != null;
1165+
if (removed) {
1166+
removedAtLeastOne.set(true);
1167+
}
1168+
}
1169+
});
1170+
1171+
if (removedAtLeastOne.get()) {
1172+
return convertRoleDescriptorsMapToBytes(roleDescriptorsMap);
1173+
} else {
1174+
// No need to serialize if we did not remove anything.
1175+
return roleDescriptorsBytes;
1176+
}
1177+
}
1178+
11341179
static boolean equivalentRealms(String name1, String type1, String name2, String type2) {
11351180
if (false == type1.equals(type2)) {
11361181
return false;

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

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -357,14 +357,6 @@ public void writeTo(StreamOutput out) throws IOException {
357357
ConfigurableClusterPrivileges.writeArray(out, getConditionalClusterPrivileges());
358358
if (out.getTransportVersion().onOrAfter(TRANSPORT_VERSION_REMOTE_INDICES)) {
359359
out.writeArray(remoteIndicesPrivileges);
360-
} else if (hasRemoteIndicesPrivileges()) {
361-
throw new IllegalArgumentException(
362-
"versions of Elasticsearch before ["
363-
+ TRANSPORT_VERSION_REMOTE_INDICES
364-
+ "] can't handle remote indices privileges and attempted to send to ["
365-
+ out.getTransportVersion()
366-
+ "]"
367-
);
368360
}
369361
}
370362

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@
1212
import org.elasticsearch.core.Nullable;
1313
import org.elasticsearch.transport.TransportRequest;
1414
import org.elasticsearch.xpack.core.security.authc.Authentication;
15+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
16+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection;
1517
import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl;
1618
import org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission.IsResourceAuthorizedPredicate;
1719
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor;
1820
import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege;
1921
import org.elasticsearch.xpack.core.security.support.Automatons;
2022

23+
import java.util.ArrayList;
2124
import java.util.Collection;
25+
import java.util.Collections;
26+
import java.util.List;
2227
import java.util.Map;
2328
import java.util.Objects;
2429
import java.util.Set;
@@ -118,6 +123,27 @@ public IndicesAccessControl authorize(
118123
return indicesAccessControl.limitIndicesAccessControl(limitedByIndicesAccessControl);
119124
}
120125

126+
@Override
127+
public RoleDescriptorsIntersection getRemoteAccessRoleDescriptorsIntersection(final String remoteClusterAlias) {
128+
final RoleDescriptorsIntersection baseIntersection = baseRole.getRemoteAccessRoleDescriptorsIntersection(remoteClusterAlias);
129+
// Intersecting with empty descriptors list should result in an empty intersection.
130+
if (baseIntersection.roleDescriptorsList().isEmpty()) {
131+
return RoleDescriptorsIntersection.EMPTY;
132+
}
133+
final RoleDescriptorsIntersection limitedByIntersection = limitedByRole.getRemoteAccessRoleDescriptorsIntersection(
134+
remoteClusterAlias
135+
);
136+
if (limitedByIntersection.roleDescriptorsList().isEmpty()) {
137+
return RoleDescriptorsIntersection.EMPTY;
138+
}
139+
final List<Set<RoleDescriptor>> mergedIntersection = new ArrayList<>(
140+
baseIntersection.roleDescriptorsList().size() + limitedByIntersection.roleDescriptorsList().size()
141+
);
142+
mergedIntersection.addAll(baseIntersection.roleDescriptorsList());
143+
mergedIntersection.addAll(limitedByIntersection.roleDescriptorsList());
144+
return new RoleDescriptorsIntersection(Collections.unmodifiableList(mergedIntersection));
145+
}
146+
121147
/**
122148
* @return A predicate that will match all the indices that this role and the limited by role has the privilege for executing the given
123149
* action on.

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.elasticsearch.xpack.core.security.authc.Authentication;
1818
import org.elasticsearch.xpack.core.security.authz.RestrictedIndices;
1919
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
20+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection;
2021
import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl;
2122
import org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission.IsResourceAuthorizedPredicate;
2223
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege;
@@ -40,6 +41,9 @@
4041

4142
public interface Role {
4243

44+
// TODO move once we have a dedicated class for RCS 2.0 constants
45+
String REMOTE_USER_ROLE_NAME = "_remote_user";
46+
4347
Role EMPTY = builder(new RestrictedIndices(Automatons.EMPTY)).build();
4448

4549
String[] names();
@@ -160,6 +164,15 @@ IndicesAccessControl authorize(
160164
FieldPermissionsCache fieldPermissionsCache
161165
);
162166

167+
/**
168+
* Returns the intersection of role descriptors defined for a remote cluster with the given alias.
169+
*
170+
* @param remoteClusterAlias the remote cluster alias for which to return a role descriptors intersection
171+
* @return an intersection of defined role descriptors for the remote access to a given cluster,
172+
* otherwise an empty intersection if remote privileges are not defined
173+
*/
174+
RoleDescriptorsIntersection getRemoteAccessRoleDescriptorsIntersection(String remoteClusterAlias);
175+
163176
/***
164177
* Creates a {@link LimitedRole} that uses this Role as base and the given role as limited-by.
165178
*/

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

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import org.apache.lucene.util.automaton.Automaton;
1010
import org.elasticsearch.cluster.metadata.IndexAbstraction;
11+
import org.elasticsearch.common.bytes.BytesReference;
1112
import org.elasticsearch.common.cache.Cache;
1213
import org.elasticsearch.common.cache.CacheBuilder;
1314
import org.elasticsearch.common.settings.Setting;
@@ -17,18 +18,24 @@
1718
import org.elasticsearch.xpack.core.security.authc.Authentication;
1819
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.PrivilegesCheckResult;
1920
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.PrivilegesToCheck;
21+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
22+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection;
2023
import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl;
2124
import org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission.IsResourceAuthorizedPredicate;
2225
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor;
2326
import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege;
2427

28+
import java.util.ArrayList;
2529
import java.util.Arrays;
2630
import java.util.Collection;
31+
import java.util.Collections;
32+
import java.util.List;
2733
import java.util.Map;
2834
import java.util.Objects;
2935
import java.util.Set;
3036
import java.util.concurrent.ExecutionException;
3137
import java.util.concurrent.atomic.AtomicReference;
38+
import java.util.stream.Collectors;
3239

3340
public class SimpleRole implements Role {
3441

@@ -168,6 +175,74 @@ public IndicesAccessControl authorize(
168175
return indices.authorize(action, requestedIndicesOrAliases, aliasAndIndexLookup, fieldPermissionsCache);
169176
}
170177

178+
@Override
179+
public RoleDescriptorsIntersection getRemoteAccessRoleDescriptorsIntersection(final String remoteClusterAlias) {
180+
final RemoteIndicesPermission remoteIndicesPermission = remoteIndices.forCluster(remoteClusterAlias);
181+
if (remoteIndicesPermission.remoteIndicesGroups().isEmpty()) {
182+
return RoleDescriptorsIntersection.EMPTY;
183+
}
184+
final List<RoleDescriptor.IndicesPrivileges> indicesPrivileges = new ArrayList<>();
185+
for (RemoteIndicesPermission.RemoteIndicesGroup remoteIndicesGroup : remoteIndicesPermission.remoteIndicesGroups()) {
186+
for (IndicesPermission.Group indicesGroup : remoteIndicesGroup.indicesPermissionGroups()) {
187+
indicesPrivileges.add(toIndicesPrivileges(indicesGroup));
188+
}
189+
}
190+
191+
return new RoleDescriptorsIntersection(
192+
new RoleDescriptor(
193+
REMOTE_USER_ROLE_NAME,
194+
null,
195+
// The role descriptors constructed here may be cached in raw byte form, using a hash of their content as a
196+
// cache key; we therefore need deterministic order when constructing them here, to ensure cache hits for
197+
// equivalent role descriptors
198+
indicesPrivileges.stream().sorted().toArray(RoleDescriptor.IndicesPrivileges[]::new),
199+
null,
200+
null,
201+
null,
202+
null,
203+
null
204+
)
205+
);
206+
}
207+
208+
private static Set<FieldPermissionsDefinition.FieldGrantExcludeGroup> getFieldGrantExcludeGroups(IndicesPermission.Group group) {
209+
if (group.getFieldPermissions().hasFieldLevelSecurity()) {
210+
final List<FieldPermissionsDefinition> fieldPermissionsDefinitions = group.getFieldPermissions()
211+
.getFieldPermissionsDefinitions();
212+
assert fieldPermissionsDefinitions.size() == 1
213+
: "a simple role can only have up to one field permissions definition per remote indices privilege";
214+
final FieldPermissionsDefinition definition = fieldPermissionsDefinitions.get(0);
215+
return definition.getFieldGrantExcludeGroups();
216+
} else {
217+
return Collections.emptySet();
218+
}
219+
}
220+
221+
private static RoleDescriptor.IndicesPrivileges toIndicesPrivileges(final IndicesPermission.Group indicesGroup) {
222+
final Set<BytesReference> queries = indicesGroup.getQuery();
223+
final Set<FieldPermissionsDefinition.FieldGrantExcludeGroup> fieldGrantExcludeGroups = getFieldGrantExcludeGroups(indicesGroup);
224+
assert queries == null || queries.size() <= 1
225+
: "translation from an indices permission group to indices privileges supports up to one DLS query but multiple queries found";
226+
assert fieldGrantExcludeGroups.size() <= 1
227+
: "translation from an indices permission group to indices privileges supports up to one FLS field-grant-exclude group"
228+
+ " but multiple groups found";
229+
230+
final BytesReference query = (queries == null || false == queries.iterator().hasNext()) ? null : queries.iterator().next();
231+
final RoleDescriptor.IndicesPrivileges.Builder builder = RoleDescriptor.IndicesPrivileges.builder()
232+
// Sort because these index privileges will be part of role descriptors that may be cached in raw byte form;
233+
// we need deterministic order to ensure cache hits for equivalent role descriptors
234+
.indices(Arrays.stream(indicesGroup.indices()).sorted().collect(Collectors.toList()))
235+
.privileges(indicesGroup.privilege().name().stream().sorted().collect(Collectors.toList()))
236+
.allowRestrictedIndices(indicesGroup.allowRestrictedIndices())
237+
.query(query);
238+
if (false == fieldGrantExcludeGroups.isEmpty()) {
239+
final FieldPermissionsDefinition.FieldGrantExcludeGroup fieldGrantExcludeGroup = fieldGrantExcludeGroups.iterator().next();
240+
builder.grantedFields(fieldGrantExcludeGroup.getGrantedFields()).deniedFields(fieldGrantExcludeGroup.getExcludedFields());
241+
}
242+
243+
return builder.build();
244+
}
245+
171246
@Override
172247
public boolean equals(Object o) {
173248
if (this == o) {

0 commit comments

Comments
 (0)