Skip to content

Commit 67f6855

Browse files
authored
Enable run as for all authentication schemes (#79809)
This PR removes the restriction for run_as so the authenticated user does not have to be authenticated by a realm. It can now also be either an API key, a token or a service account. Note we don't currently have a service account that has run_as privilege. This PR also include a change to the existing internal behaviour: the final authentication object created for run-as now keeps information of the authenticated user instead of dropping them. For example, metadata associated with the original authentication is preserved. If the original authentication is an API key, the final authentication will have authentication type as "api_key" as well.
1 parent a80be4d commit 67f6855

File tree

14 files changed

+279
-45
lines changed

14 files changed

+279
-45
lines changed

x-pack/plugin/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ tasks.named("yamlRestTestV7CompatTransform").configure{ task ->
113113
task.skipTest("indices.freeze/10_basic/Basic", "#70192 -- the freeze index API is removed from 8.0")
114114
task.skipTest("indices.freeze/10_basic/Test index options", "#70192 -- the freeze index API is removed from 8.0")
115115
task.skipTest("ml/categorization_agg/Test categorization aggregation with poor settings", "https://github.com/elastic/elasticsearch/pull/79586")
116+
task.skipTest("service_accounts/10_basic/Test service account tokens", "mute till we decide whether to make the changes in 7.16")
116117

117118
task.replaceValueInMatch("_type", "_doc")
118119
task.addAllowedWarningRegex("\\[types removal\\].*")

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,11 @@ public Map<String, Object> getMetadata() {
113113
return metadata;
114114
}
115115

116-
public boolean isServiceAccount() {
117-
return ServiceAccountSettings.REALM_TYPE.equals(getAuthenticatedBy().getType()) && null == getLookedUpBy();
116+
public boolean isAuthenticatedWithServiceAccount() {
117+
return ServiceAccountSettings.REALM_TYPE.equals(getAuthenticatedBy().getType());
118118
}
119119

120-
public boolean isApiKey() {
120+
public boolean isAuthenticatedWithApiKey() {
121121
return AuthenticationType.API_KEY.equals(getAuthenticationType());
122122
}
123123

@@ -235,7 +235,7 @@ public void toXContentFragment(XContentBuilder builder) throws IOException {
235235
builder.array(User.Fields.ROLES.getPreferredName(), user.roles());
236236
builder.field(User.Fields.FULL_NAME.getPreferredName(), user.fullName());
237237
builder.field(User.Fields.EMAIL.getPreferredName(), user.email());
238-
if (isServiceAccount()) {
238+
if (isAuthenticatedWithServiceAccount()) {
239239
final String tokenName = (String) getMetadata().get(ServiceAccountSettings.TOKEN_NAME_FIELD);
240240
assert tokenName != null : "token name cannot be null";
241241
final String tokenSource = (String) getMetadata().get(ServiceAccountSettings.TOKEN_SOURCE_FIELD);
@@ -261,7 +261,7 @@ public void toXContentFragment(XContentBuilder builder) throws IOException {
261261
}
262262
builder.endObject();
263263
builder.field(User.Fields.AUTHENTICATION_TYPE.getPreferredName(), getAuthenticationType().name().toLowerCase(Locale.ROOT));
264-
if (isApiKey()) {
264+
if (isAuthenticatedWithApiKey()) {
265265
this.assertApiKeyMetadata();
266266
final String apiKeyId = (String) this.metadata.get(AuthenticationField.API_KEY_ID_KEY);
267267
final String apiKeyName = (String) this.metadata.get(AuthenticationField.API_KEY_NAME_KEY);

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,10 @@ public void testIsServiceAccount() {
126126
);
127127
final Authentication authentication = new Authentication(user, authRealm, lookupRealm);
128128

129-
if (authRealmIsForServiceAccount && lookupRealm == null) {
130-
assertThat(authentication.isServiceAccount(), is(true));
129+
if (authRealmIsForServiceAccount) {
130+
assertThat(authentication.isAuthenticatedWithServiceAccount(), is(true));
131131
} else {
132-
assertThat(authentication.isServiceAccount(), is(false));
132+
assertThat(authentication.isAuthenticatedWithServiceAccount(), is(false));
133133
}
134134
}
135135

x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/RunAsIntegTests.java

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,35 @@
88

99
import org.elasticsearch.client.Request;
1010
import org.elasticsearch.client.RequestOptions;
11+
import org.elasticsearch.client.Response;
1112
import org.elasticsearch.client.ResponseException;
13+
import org.elasticsearch.common.settings.Settings;
1214
import org.elasticsearch.test.SecurityIntegTestCase;
1315
import org.elasticsearch.test.SecuritySettingsSource;
16+
import org.elasticsearch.test.XContentTestUtils;
17+
import org.elasticsearch.xpack.core.XPackSettings;
1418
import org.elasticsearch.xpack.core.security.authc.AuthenticationServiceField;
1519
import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
1620
import org.junit.BeforeClass;
1721

22+
import java.io.IOException;
23+
1824
import static org.elasticsearch.test.SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING;
25+
import static org.hamcrest.Matchers.equalTo;
1926
import static org.hamcrest.Matchers.is;
2027

2128
public class RunAsIntegTests extends SecurityIntegTestCase {
2229

2330
private static final String RUN_AS_USER = "run_as_user";
2431
private static final String CLIENT_USER = "transport_user";
25-
private static final String ROLES = "run_as_role:\n" + " run_as: [ '" + SecuritySettingsSource.TEST_USER_NAME + "', 'idontexist' ]\n";
32+
private static final String NO_ROLE_USER = "no_role_user";
33+
private static final String ROLES = "run_as_role:\n"
34+
+ " cluster: ['manage_own_api_key', 'manage_token']\n"
35+
+ " run_as: [ '"
36+
+ SecuritySettingsSource.TEST_USER_NAME
37+
+ "', '"
38+
+ NO_ROLE_USER
39+
+ "', 'idontexist' ]\n";
2640

2741
// indicates whether the RUN_AS_USER that is being authenticated is also a superuser
2842
private static boolean runAsHasSuperUserRole;
@@ -52,6 +66,10 @@ public String configUsers() {
5266
+ CLIENT_USER
5367
+ ":"
5468
+ SecuritySettingsSource.TEST_PASSWORD_HASHED
69+
+ "\n"
70+
+ NO_ROLE_USER
71+
+ ":"
72+
+ SecuritySettingsSource.TEST_PASSWORD_HASHED
5573
+ "\n";
5674
}
5775

@@ -64,6 +82,13 @@ public String configUsersRoles() {
6482
return roles;
6583
}
6684

85+
@Override
86+
protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) {
87+
final Settings.Builder builder = Settings.builder().put(super.nodeSettings(nodeOrdinal, otherSettings));
88+
builder.put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), "true");
89+
return builder.build();
90+
}
91+
6792
@Override
6893
protected boolean transportSSLEnabled() {
6994
return false;
@@ -119,6 +144,107 @@ public void testNonExistentRunAsUserUsingHttp() throws Exception {
119144
}
120145
}
121146

147+
public void testRunAsUsingApiKey() throws IOException {
148+
final Request createApiKeyRequest = new Request("PUT", "/_security/api_key");
149+
createApiKeyRequest.setJsonEntity("{\"name\":\"k1\"}\n");
150+
createApiKeyRequest.setOptions(
151+
createApiKeyRequest.getOptions()
152+
.toBuilder()
153+
.addHeader("Authorization", UsernamePasswordToken.basicAuthHeaderValue(RUN_AS_USER, TEST_PASSWORD_SECURE_STRING))
154+
);
155+
final Response createApiKeyResponse = getRestClient().performRequest(createApiKeyRequest);
156+
final XContentTestUtils.JsonMapView apiKeyMapView = XContentTestUtils.createJsonMapView(
157+
createApiKeyResponse.getEntity().getContent()
158+
);
159+
160+
final boolean runAsTestUser = false;
161+
162+
final Request authenticateRequest = new Request("GET", "/_security/_authenticate");
163+
authenticateRequest.setOptions(
164+
authenticateRequest.getOptions()
165+
.toBuilder()
166+
.addHeader("Authorization", "ApiKey " + apiKeyMapView.get("encoded"))
167+
.addHeader(
168+
AuthenticationServiceField.RUN_AS_USER_HEADER,
169+
runAsTestUser ? SecuritySettingsSource.TEST_USER_NAME : NO_ROLE_USER
170+
)
171+
);
172+
final Response authenticateResponse = getRestClient().performRequest(authenticateRequest);
173+
final XContentTestUtils.JsonMapView authenticateJsonView = XContentTestUtils.createJsonMapView(
174+
authenticateResponse.getEntity().getContent()
175+
);
176+
assertThat(authenticateJsonView.get("username"), equalTo(runAsTestUser ? SecuritySettingsSource.TEST_USER_NAME : NO_ROLE_USER));
177+
assertThat(authenticateJsonView.get("authentication_realm.type"), equalTo("_es_api_key"));
178+
assertThat(authenticateJsonView.get("authentication_type"), equalTo("api_key"));
179+
180+
final Request getUserRequest = new Request("GET", "/_security/user");
181+
getUserRequest.setOptions(
182+
getUserRequest.getOptions()
183+
.toBuilder()
184+
.addHeader("Authorization", "ApiKey " + apiKeyMapView.get("encoded"))
185+
.addHeader(
186+
AuthenticationServiceField.RUN_AS_USER_HEADER,
187+
runAsTestUser ? SecuritySettingsSource.TEST_USER_NAME : NO_ROLE_USER
188+
)
189+
);
190+
if (runAsTestUser) {
191+
assertThat(getRestClient().performRequest(getUserRequest).getStatusLine().getStatusCode(), equalTo(200));
192+
} else {
193+
final ResponseException e = expectThrows(ResponseException.class, () -> getRestClient().performRequest(getUserRequest));
194+
assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(403));
195+
}
196+
}
197+
198+
public void testRunAsUsingOAuthToken() throws IOException {
199+
final Request createTokenRequest = new Request("POST", "/_security/oauth2/token");
200+
createTokenRequest.setJsonEntity("{\"grant_type\":\"client_credentials\"}");
201+
createTokenRequest.setOptions(
202+
createTokenRequest.getOptions()
203+
.toBuilder()
204+
.addHeader("Authorization", UsernamePasswordToken.basicAuthHeaderValue(RUN_AS_USER, TEST_PASSWORD_SECURE_STRING))
205+
);
206+
final Response createTokenResponse = getRestClient().performRequest(createTokenRequest);
207+
final XContentTestUtils.JsonMapView tokenMapView = XContentTestUtils.createJsonMapView(
208+
createTokenResponse.getEntity().getContent()
209+
);
210+
211+
final boolean runAsTestUser = randomBoolean();
212+
213+
final Request authenticateRequest = new Request("GET", "/_security/_authenticate");
214+
authenticateRequest.setOptions(
215+
authenticateRequest.getOptions()
216+
.toBuilder()
217+
.addHeader("Authorization", "Bearer " + tokenMapView.get("access_token"))
218+
.addHeader(
219+
AuthenticationServiceField.RUN_AS_USER_HEADER,
220+
runAsTestUser ? SecuritySettingsSource.TEST_USER_NAME : NO_ROLE_USER
221+
)
222+
);
223+
final Response authenticateResponse = getRestClient().performRequest(authenticateRequest);
224+
final XContentTestUtils.JsonMapView authenticateJsonView = XContentTestUtils.createJsonMapView(
225+
authenticateResponse.getEntity().getContent()
226+
);
227+
assertThat(authenticateJsonView.get("username"), equalTo(runAsTestUser ? SecuritySettingsSource.TEST_USER_NAME : NO_ROLE_USER));
228+
assertThat(authenticateJsonView.get("authentication_type"), equalTo("token"));
229+
230+
final Request getUserRequest = new Request("GET", "/_security/user");
231+
getUserRequest.setOptions(
232+
getUserRequest.getOptions()
233+
.toBuilder()
234+
.addHeader("Authorization", "Bearer " + tokenMapView.get("access_token"))
235+
.addHeader(
236+
AuthenticationServiceField.RUN_AS_USER_HEADER,
237+
runAsTestUser ? SecuritySettingsSource.TEST_USER_NAME : NO_ROLE_USER
238+
)
239+
);
240+
if (runAsTestUser) {
241+
assertThat(getRestClient().performRequest(getUserRequest).getStatusLine().getStatusCode(), equalTo(200));
242+
} else {
243+
final ResponseException e = expectThrows(ResponseException.class, () -> getRestClient().performRequest(getUserRequest));
244+
assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(403));
245+
}
246+
}
247+
122248
private static Request requestForUserRunAsUser(String user) {
123249
Request request = new Request("GET", "/_nodes");
124250
RequestOptions.Builder options = request.getOptions().toBuilder();

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenAction.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@ protected void doExecute(Task task, CreateTokenRequest request, ActionListener<C
7575
break;
7676
case CLIENT_CREDENTIALS:
7777
Authentication authentication = securityContext.getAuthentication();
78-
if (authentication.isServiceAccount()) {
78+
if (authentication.isAuthenticatedWithServiceAccount() && false == authentication.getUser().isRunAs()) {
79+
// Service account itself cannot create OAuth2 tokens.
80+
// But it is possible to create an oauth2 token if the service account run-as a different user.
81+
// In this case, the token will be created for the run-as user (not the service account).
7982
listener.onFailure(new ElasticsearchException("OAuth2 token creation is not supported for service accounts"));
8083
return;
8184
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1512,7 +1512,7 @@ LogEntryBuilder withAuthentication(Authentication authentication) {
15121512
logEntry.with(PRINCIPAL_REALM_FIELD_NAME, authentication.getAuthenticatedBy().getName());
15131513
}
15141514
}
1515-
if (authentication.isServiceAccount()) {
1515+
if (authentication.isAuthenticatedWithServiceAccount()) {
15161516
logEntry.with(SERVICE_TOKEN_NAME_FIELD_NAME, (String) authentication.getMetadata().get(TOKEN_NAME_FIELD))
15171517
.with(
15181518
SERVICE_TOKEN_TYPE_FIELD_NAME,

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,7 @@ private void maybeLookupRunAsUser(
196196
Authentication authentication,
197197
ActionListener<Authentication> listener
198198
) {
199-
// TODO: only allow run as for realm authentication to maintain the existing behaviour
200-
if (false == runAsEnabled || authentication.getAuthenticationType() != Authentication.AuthenticationType.REALM) {
199+
if (false == runAsEnabled) {
201200
finishAuthentication(context, authentication, listener);
202201
return;
203202
}
@@ -227,9 +226,23 @@ private void maybeLookupRunAsUser(
227226
if (tuple == null) {
228227
logger.debug("Cannot find run-as user [{}] for authenticated user [{}]", runAsUsername, user.principal());
229228
// the user does not exist, but we still create a User object, which will later be rejected by authz
230-
finalAuth = new Authentication(new User(runAsUsername, null, user), authentication.getAuthenticatedBy(), null);
229+
finalAuth = new Authentication(
230+
new User(runAsUsername, null, user),
231+
authentication.getAuthenticatedBy(),
232+
null,
233+
authentication.getVersion(),
234+
authentication.getAuthenticationType(),
235+
authentication.getMetadata()
236+
);
231237
} else {
232-
finalAuth = new Authentication(new User(tuple.v1(), user), authentication.getAuthenticatedBy(), tuple.v2());
238+
finalAuth = new Authentication(
239+
new User(tuple.v1(), user),
240+
authentication.getAuthenticatedBy(),
241+
tuple.v2(),
242+
authentication.getVersion(),
243+
authentication.getAuthenticationType(),
244+
authentication.getMetadata()
245+
);
233246
}
234247
finishAuthentication(context, finalAuth, listener);
235248
}, listener::onFailure));

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ public void findTokensFor(GetServiceAccountCredentialsRequest request, ActionLis
164164
}
165165

166166
public void getRoleDescriptor(Authentication authentication, ActionListener<RoleDescriptor> listener) {
167-
assert authentication.isServiceAccount() : "authentication is not for service account: " + authentication;
167+
assert authentication.isAuthenticatedWithServiceAccount() : "authentication is not for service account: " + authentication;
168168
final String principal = authentication.getUser().principal();
169169
final ServiceAccount account = ACCOUNTS.get(principal);
170170
if (account == null) {

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,10 @@ private ElasticsearchSecurityException denialException(
882882
}
883883
}
884884

885-
String userText = "user [" + authUser.principal() + "]";
885+
String userText = (authentication.isAuthenticatedWithServiceAccount() ? "service account" : "user")
886+
+ " ["
887+
+ authUser.principal()
888+
+ "]";
886889
// check for run as
887890
if (authentication.getUser().isRunAs()) {
888891
userText = userText + " run as [" + authentication.getUser().principal() + "]";
@@ -892,9 +895,14 @@ private ElasticsearchSecurityException denialException(
892895
final String apiKeyId = (String) authentication.getMetadata().get(AuthenticationField.API_KEY_ID_KEY);
893896
assert apiKeyId != null : "api key id must be present in the metadata";
894897
userText = "API key id [" + apiKeyId + "] of " + userText;
895-
} else if (false == authentication.isServiceAccount()) {
896-
// Don't print roles for API keys because they're not meaningful
897-
// Also not printing roles for service accounts since they have no roles
898+
}
899+
900+
// The run-as user is always from a realm. So it must have roles that can be printed.
901+
// If the user is not run-as, we cannot print the roles if it's an API key or a service account (both do not have
902+
// roles, but privileges)
903+
if (authentication.getUser().isRunAs()
904+
|| (false == authentication.isAuthenticatedWithServiceAccount()
905+
&& AuthenticationType.API_KEY != authentication.getAuthenticationType())) {
898906
userText = userText + " with roles [" + Strings.arrayToCommaDelimitedString(authentication.getUser().roles()) + "]";
899907
}
900908

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

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -281,26 +281,37 @@ public void getRoles(User user, Authentication authentication, ActionListener<Ro
281281
return;
282282
}
283283

284-
if (authentication.isServiceAccount()) {
285-
getRolesForServiceAccount(authentication, roleActionListener);
286-
} else if (ApiKeyService.isApiKeyAuthentication(authentication)) {
287-
getRolesForApiKey(authentication, roleActionListener);
284+
if (user.isRunAs()) {
285+
// The runas user currently must be from a realm and have regular roles
286+
getRolesForUser(user, roleActionListener);
288287
} else {
289-
Set<String> roleNames = new HashSet<>(Arrays.asList(user.roles()));
290-
if (isAnonymousEnabled && anonymousUser.equals(user) == false) {
291-
if (anonymousUser.roles().length == 0) {
292-
throw new IllegalStateException("anonymous is only enabled when the anonymous user has roles");
293-
}
294-
Collections.addAll(roleNames, anonymousUser.roles());
288+
// The authenticated user may not come from a realm and they need to be handled specially
289+
if (authentication.isAuthenticatedWithServiceAccount()) {
290+
getRolesForServiceAccount(authentication, roleActionListener);
291+
} else if (ApiKeyService.isApiKeyAuthentication(authentication)) {
292+
// API key role descriptors are stored in the authentication metadata
293+
getRolesForApiKey(authentication, roleActionListener);
294+
} else {
295+
getRolesForUser(user, roleActionListener);
295296
}
297+
}
298+
}
296299

297-
if (roleNames.isEmpty()) {
298-
roleActionListener.onResponse(Role.EMPTY);
299-
} else if (roleNames.contains(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName())) {
300-
roleActionListener.onResponse(superuserRole);
301-
} else {
302-
roles(roleNames, roleActionListener);
300+
private void getRolesForUser(User user, ActionListener<Role> roleActionListener) {
301+
Set<String> roleNames = new HashSet<>(Arrays.asList(user.roles()));
302+
if (isAnonymousEnabled && anonymousUser.equals(user) == false) {
303+
if (anonymousUser.roles().length == 0) {
304+
throw new IllegalStateException("anonymous is only enabled when the anonymous user has roles");
303305
}
306+
Collections.addAll(roleNames, anonymousUser.roles());
307+
}
308+
309+
if (roleNames.isEmpty()) {
310+
roleActionListener.onResponse(Role.EMPTY);
311+
} else if (roleNames.contains(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName())) {
312+
roleActionListener.onResponse(superuserRole);
313+
} else {
314+
roles(roleNames, roleActionListener);
304315
}
305316
}
306317

0 commit comments

Comments
 (0)