Skip to content

Commit fd4c617

Browse files
authored
Build role for remote access authentication (#93316)
This PR adds support for building roles for remote_access authentication instances, under the new remote cluster security model. This change is stand-alone and not wired up to active code flows yet. A proof of concept in #92089 highlights how the model change in this PR fits into the broader context of the fulfilling cluster processing cross cluster requests.
1 parent 89467ea commit fd4c617

File tree

10 files changed

+346
-28
lines changed

10 files changed

+346
-28
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.apache.lucene.util.BytesRef;
1111
import org.elasticsearch.TransportVersion;
1212
import org.elasticsearch.common.bytes.AbstractBytesReference;
13+
import org.elasticsearch.common.bytes.BytesArray;
1314
import org.elasticsearch.common.bytes.BytesReference;
1415
import org.elasticsearch.common.io.stream.BytesStreamOutput;
1516
import org.elasticsearch.common.io.stream.StreamInput;
@@ -148,6 +149,8 @@ public Map<String, Object> copyWithRemoteAccessEntries(final Map<String, Object>
148149
}
149150

150151
public static final class RoleDescriptorsBytes extends AbstractBytesReference {
152+
153+
public static final RoleDescriptorsBytes EMPTY = new RoleDescriptorsBytes(new BytesArray("{}"));
151154
private final BytesReference rawBytes;
152155

153156
public RoleDescriptorsBytes(BytesReference rawBytes) {

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
2020
import org.elasticsearch.xpack.core.security.user.User;
2121

22+
import java.util.ArrayList;
23+
import java.util.List;
2224
import java.util.Map;
2325
import java.util.Objects;
2426

@@ -105,8 +107,7 @@ public RoleReferenceIntersection getRoleReferenceIntersection(@Nullable Anonymou
105107
case SERVICE_ACCOUNT:
106108
return new RoleReferenceIntersection(new RoleReference.ServiceAccountRoleReference(user.principal()));
107109
case REMOTE_ACCESS:
108-
assert false : "unsupported subject type: [" + type + "]";
109-
throw new UnsupportedOperationException("unsupported subject type: [" + type + "]");
110+
return buildRoleReferencesForRemoteAccess();
110111
default:
111112
assert false : "unknown subject type: [" + type + "]";
112113
throw new IllegalStateException("unknown subject type: [" + type + "]");
@@ -231,6 +232,27 @@ private RoleReferenceIntersection buildRoleReferencesForApiKey() {
231232
);
232233
}
233234

235+
private RoleReferenceIntersection buildRoleReferencesForRemoteAccess() {
236+
final List<RoleReference> roleReferences = new ArrayList<>(4);
237+
@SuppressWarnings("unchecked")
238+
final List<RemoteAccessAuthentication.RoleDescriptorsBytes> remoteAccessRoleDescriptorsBytes = (List<
239+
RemoteAccessAuthentication.RoleDescriptorsBytes>) metadata.get(AuthenticationField.REMOTE_ACCESS_ROLE_DESCRIPTORS_KEY);
240+
if (remoteAccessRoleDescriptorsBytes.isEmpty()) {
241+
// If the remote access role descriptors are empty, the remote user has no privileges. We need to add an empty role to restrict
242+
// access of the overall intersection accordingly
243+
roleReferences.add(new RoleReference.RemoteAccessRoleReference(RemoteAccessAuthentication.RoleDescriptorsBytes.EMPTY));
244+
} else {
245+
// TODO handle this once we support API keys as querying subjects
246+
assert remoteAccessRoleDescriptorsBytes.size() == 1
247+
: "only a singleton list of remote access role descriptors bytes is supported";
248+
for (RemoteAccessAuthentication.RoleDescriptorsBytes roleDescriptorsBytes : remoteAccessRoleDescriptorsBytes) {
249+
roleReferences.add(new RoleReference.RemoteAccessRoleReference(roleDescriptorsBytes));
250+
}
251+
}
252+
roleReferences.addAll(buildRoleReferencesForApiKey().getRoleReferences());
253+
return new RoleReferenceIntersection(List.copyOf(roleReferences));
254+
}
255+
234256
private static boolean isEmptyRoleDescriptorsBytes(BytesReference roleDescriptorsBytes) {
235257
return roleDescriptorsBytes == null || (roleDescriptorsBytes.length() == 2 && "{}".equals(roleDescriptorsBytes.utf8ToString()));
236258
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ public record RoleDescriptorsIntersection(Collection<Set<RoleDescriptor>> roleDe
2626

2727
public static RoleDescriptorsIntersection EMPTY = new RoleDescriptorsIntersection(Collections.emptyList());
2828

29+
public RoleDescriptorsIntersection(RoleDescriptor roleDescriptor) {
30+
this(List.of(Set.of(roleDescriptor)));
31+
}
32+
2933
public RoleDescriptorsIntersection(StreamInput in) throws IOException {
3034
this(in.readImmutableList(inner -> inner.readSet(RoleDescriptor::new)));
3135
}

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.elasticsearch.action.ActionListener;
1111
import org.elasticsearch.common.bytes.BytesReference;
1212
import org.elasticsearch.common.hash.MessageDigests;
13+
import org.elasticsearch.xpack.core.security.authc.RemoteAccessAuthentication;
1314

1415
import java.util.HashSet;
1516
import java.util.List;
@@ -116,6 +117,38 @@ public ApiKeyRoleType getRoleType() {
116117
}
117118
}
118119

120+
final class RemoteAccessRoleReference implements RoleReference {
121+
122+
private final RemoteAccessAuthentication.RoleDescriptorsBytes roleDescriptorsBytes;
123+
private RoleKey id = null;
124+
125+
public RemoteAccessRoleReference(RemoteAccessAuthentication.RoleDescriptorsBytes roleDescriptorsBytes) {
126+
this.roleDescriptorsBytes = roleDescriptorsBytes;
127+
}
128+
129+
@Override
130+
public RoleKey id() {
131+
// Hashing can be expensive. memorize the result in case the method is called multiple times.
132+
if (id == null) {
133+
final String roleDescriptorsHash = MessageDigests.toHexString(
134+
MessageDigests.digest(roleDescriptorsBytes, MessageDigests.sha256())
135+
);
136+
id = new RoleKey(Set.of("remote_access:" + roleDescriptorsHash), "remote_access");
137+
}
138+
return id;
139+
}
140+
141+
@Override
142+
public void resolve(RoleReferenceResolver resolver, ActionListener<RolesRetrievalResult> listener) {
143+
resolver.resolveRemoteAccessRoleReference(this, listener);
144+
}
145+
146+
public RemoteAccessAuthentication.RoleDescriptorsBytes getRoleDescriptorsBytes() {
147+
return roleDescriptorsBytes;
148+
}
149+
150+
}
151+
119152
/**
120153
* Same as {@link ApiKeyRoleReference} but for BWC purpose (prior to v7.9.0)
121154
*/

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,9 @@ void resolveBwcApiKeyRoleReference(
2525
);
2626

2727
void resolveServiceAccountRoleReference(ServiceAccountRoleReference roleReference, ActionListener<RolesRetrievalResult> listener);
28+
29+
void resolveRemoteAccessRoleReference(
30+
RoleReference.RemoteAccessRoleReference remoteAccessRoleReference,
31+
ActionListener<RolesRetrievalResult> listener
32+
);
2833
}

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTestHelper.java

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -237,43 +237,46 @@ public static String randomInternalRoleName() {
237237
);
238238
}
239239

240-
public static RemoteAccessAuthentication randomRemoteAccessAuthentication() {
240+
public static RemoteAccessAuthentication randomRemoteAccessAuthentication(RoleDescriptorsIntersection roleDescriptorsIntersection) {
241241
try {
242242
// TODO add apikey() once we have querying-cluster-side API key support
243243
final Authentication authentication = ESTestCase.randomFrom(
244244
AuthenticationTestHelper.builder().realm(),
245245
AuthenticationTestHelper.builder().internal(SystemUser.INSTANCE)
246246
).build();
247-
return new RemoteAccessAuthentication(
248-
authentication,
249-
new RoleDescriptorsIntersection(
250-
List.of(
251-
// TODO randomize to add a second set once we have querying-cluster-side API key support
252-
Set.of(
253-
new RoleDescriptor(
254-
"a",
255-
null,
256-
new RoleDescriptor.IndicesPrivileges[] {
257-
RoleDescriptor.IndicesPrivileges.builder()
258-
.indices("index1")
259-
.privileges("read", "read_cross_cluster")
260-
.build() },
261-
null,
262-
null,
263-
null,
264-
null,
265-
null,
266-
null
267-
)
268-
)
269-
)
270-
)
271-
);
247+
return new RemoteAccessAuthentication(authentication, roleDescriptorsIntersection);
272248
} catch (IOException e) {
273249
throw new UncheckedIOException(e);
274250
}
275251
}
276252

253+
public static RemoteAccessAuthentication randomRemoteAccessAuthentication() {
254+
return randomRemoteAccessAuthentication(
255+
new RoleDescriptorsIntersection(
256+
List.of(
257+
// TODO randomize to add a second set once we have querying-cluster-side API key support
258+
Set.of(
259+
new RoleDescriptor(
260+
"_remote_user",
261+
null,
262+
new RoleDescriptor.IndicesPrivileges[] {
263+
RoleDescriptor.IndicesPrivileges.builder()
264+
.indices("index1")
265+
.privileges("read", "read_cross_cluster")
266+
.build() },
267+
null,
268+
null,
269+
null,
270+
null,
271+
null,
272+
null
273+
)
274+
)
275+
)
276+
)
277+
);
278+
}
279+
277280
public static class AuthenticationTestBuilder {
278281
private TransportVersion transportVersion;
279282
private Authentication authenticatingAuthentication;

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/SubjectTests.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.elasticsearch.test.ESTestCase;
1717
import org.elasticsearch.test.TransportVersionUtils;
1818
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
19+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection;
1920
import org.elasticsearch.xpack.core.security.authz.store.RoleReference;
2021
import org.elasticsearch.xpack.core.security.authz.store.RoleReference.ApiKeyRoleReference;
2122
import org.elasticsearch.xpack.core.security.authz.store.RoleReference.BwcApiKeyRoleReference;
@@ -35,7 +36,10 @@
3536
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_REALM_NAME;
3637
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_REALM_TYPE;
3738
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY;
39+
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.REMOTE_ACCESS_REALM_NAME;
40+
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.REMOTE_ACCESS_REALM_TYPE;
3841
import static org.elasticsearch.xpack.core.security.authc.Subject.FLEET_SERVER_ROLE_DESCRIPTOR_BYTES_V_7_14;
42+
import static org.elasticsearch.xpack.core.security.authz.store.RoleReference.RemoteAccessRoleReference;
3943
import static org.hamcrest.Matchers.arrayContaining;
4044
import static org.hamcrest.Matchers.contains;
4145
import static org.hamcrest.Matchers.equalTo;
@@ -145,6 +149,81 @@ public void testGetRoleReferencesForApiKey() {
145149
}
146150
}
147151

152+
public void testGetRoleReferencesForRemoteAccess() {
153+
Map<String, Object> authMetadata = new HashMap<>();
154+
final String apiKeyId = randomAlphaOfLength(12);
155+
authMetadata.put(AuthenticationField.API_KEY_ID_KEY, apiKeyId);
156+
authMetadata.put(AuthenticationField.API_KEY_NAME_KEY, randomBoolean() ? null : randomAlphaOfLength(12));
157+
final BytesReference roleBytes = new BytesArray("""
158+
{"role":{"indices":[{"names":["index*"],"privileges":["read"]}]}}""");
159+
final BytesReference limitedByRoleBytes = new BytesArray("""
160+
{"limited-by-role":{"indices":[{"names":["*"],"privileges":["all"]}]}}""");
161+
162+
final boolean emptyRoleBytes = randomBoolean();
163+
164+
authMetadata.put(
165+
AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY,
166+
emptyRoleBytes ? randomFrom(Arrays.asList(null, new BytesArray("{}"))) : roleBytes
167+
);
168+
authMetadata.put(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, limitedByRoleBytes);
169+
170+
final RemoteAccessAuthentication remoteAccessAuthentication = randomBoolean()
171+
? AuthenticationTestHelper.randomRemoteAccessAuthentication(RoleDescriptorsIntersection.EMPTY)
172+
: AuthenticationTestHelper.randomRemoteAccessAuthentication();
173+
authMetadata = remoteAccessAuthentication.copyWithRemoteAccessEntries(authMetadata);
174+
175+
final Subject subject = new Subject(
176+
new User("joe"),
177+
new Authentication.RealmRef(REMOTE_ACCESS_REALM_NAME, REMOTE_ACCESS_REALM_TYPE, "node"),
178+
TransportVersion.CURRENT,
179+
authMetadata
180+
);
181+
182+
final RoleReferenceIntersection roleReferenceIntersection = subject.getRoleReferenceIntersection(getAnonymousUser());
183+
final List<RoleReference> roleReferences = roleReferenceIntersection.getRoleReferences();
184+
if (emptyRoleBytes) {
185+
assertThat(roleReferences, contains(isA(RemoteAccessRoleReference.class), isA(ApiKeyRoleReference.class)));
186+
187+
final RemoteAccessRoleReference remoteAccessRoleReference = (RemoteAccessRoleReference) roleReferences.get(0);
188+
assertThat(
189+
remoteAccessRoleReference.getRoleDescriptorsBytes(),
190+
equalTo(
191+
remoteAccessAuthentication.getRoleDescriptorsBytesList().isEmpty()
192+
? RemoteAccessAuthentication.RoleDescriptorsBytes.EMPTY
193+
: remoteAccessAuthentication.getRoleDescriptorsBytesList().get(0)
194+
)
195+
);
196+
197+
final ApiKeyRoleReference roleReference = (ApiKeyRoleReference) roleReferences.get(1);
198+
assertThat(roleReference.getApiKeyId(), equalTo(apiKeyId));
199+
assertThat(roleReference.getRoleDescriptorsBytes(), equalTo(authMetadata.get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY)));
200+
201+
} else {
202+
assertThat(
203+
roleReferences,
204+
contains(isA(RemoteAccessRoleReference.class), isA(ApiKeyRoleReference.class), isA(ApiKeyRoleReference.class))
205+
);
206+
207+
final RemoteAccessRoleReference remoteAccessRoleReference = (RemoteAccessRoleReference) roleReferences.get(0);
208+
assertThat(
209+
remoteAccessRoleReference.getRoleDescriptorsBytes(),
210+
equalTo(
211+
remoteAccessAuthentication.getRoleDescriptorsBytesList().isEmpty()
212+
? RemoteAccessAuthentication.RoleDescriptorsBytes.EMPTY
213+
: remoteAccessAuthentication.getRoleDescriptorsBytesList().get(0)
214+
)
215+
);
216+
217+
final ApiKeyRoleReference roleReference = (ApiKeyRoleReference) roleReferences.get(1);
218+
assertThat(roleReference.getApiKeyId(), equalTo(apiKeyId));
219+
assertThat(roleReference.getRoleDescriptorsBytes(), equalTo(authMetadata.get(API_KEY_ROLE_DESCRIPTORS_KEY)));
220+
221+
final ApiKeyRoleReference limitedByRoleReference = (ApiKeyRoleReference) roleReferences.get(2);
222+
assertThat(limitedByRoleReference.getApiKeyId(), equalTo(apiKeyId));
223+
assertThat(limitedByRoleReference.getRoleDescriptorsBytes(), equalTo(authMetadata.get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY)));
224+
}
225+
}
226+
148227
public void testGetRoleReferencesForApiKeyBwc() {
149228
Map<String, Object> authMetadata = new HashMap<>();
150229
final String apiKeyId = randomAlphaOfLength(12);

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/RoleReferenceTests.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.elasticsearch.common.bytes.BytesArray;
1111
import org.elasticsearch.common.hash.MessageDigests;
1212
import org.elasticsearch.test.ESTestCase;
13+
import org.elasticsearch.xpack.core.security.authc.RemoteAccessAuthentication;
1314

1415
import java.util.Set;
1516

@@ -65,6 +66,18 @@ public void testApiKeyRoleReference() {
6566
assertThat(roleKey.getSource(), equalTo("apikey_" + apiKeyRoleType));
6667
}
6768

69+
public void testRemoteAccessRoleReference() {
70+
final var roleDescriptorsBytes = new RemoteAccessAuthentication.RoleDescriptorsBytes(new BytesArray(randomAlphaOfLength(50)));
71+
final var remoteAccessRoleReference = new RoleReference.RemoteAccessRoleReference(roleDescriptorsBytes);
72+
73+
final RoleKey roleKey = remoteAccessRoleReference.id();
74+
assertThat(
75+
roleKey.getNames(),
76+
hasItem("remote_access:" + MessageDigests.toHexString(MessageDigests.digest(roleDescriptorsBytes, MessageDigests.sha256())))
77+
);
78+
assertThat(roleKey.getSource(), equalTo("remote_access"));
79+
}
80+
6881
public void testServiceAccountRoleReference() {
6982
final String principal = randomAlphaOfLength(8) + "/" + randomAlphaOfLength(8);
7083
final RoleReference.ServiceAccountRoleReference serviceAccountRoleReference = new RoleReference.ServiceAccountRoleReference(

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/RoleDescriptorStore.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,21 @@ public void resolveServiceAccountRoleReference(
132132
}));
133133
}
134134

135+
@Override
136+
public void resolveRemoteAccessRoleReference(
137+
RoleReference.RemoteAccessRoleReference remoteAccessRoleReference,
138+
ActionListener<RolesRetrievalResult> listener
139+
) {
140+
final Set<RoleDescriptor> roleDescriptors = remoteAccessRoleReference.getRoleDescriptorsBytes().toRoleDescriptors();
141+
if (roleDescriptors.isEmpty()) {
142+
listener.onResponse(RolesRetrievalResult.EMPTY);
143+
return;
144+
}
145+
final RolesRetrievalResult rolesRetrievalResult = new RolesRetrievalResult();
146+
rolesRetrievalResult.addDescriptors(Set.copyOf(roleDescriptors));
147+
listener.onResponse(rolesRetrievalResult);
148+
}
149+
135150
private void resolveRoleNames(Set<String> roleNames, ActionListener<RolesRetrievalResult> listener) {
136151
roleDescriptors(roleNames, ActionListener.wrap(rolesRetrievalResult -> {
137152
logDeprecatedRoles(rolesRetrievalResult.getRoleDescriptors());

0 commit comments

Comments
 (0)