Skip to content

Commit 2c1b34b

Browse files
Fix can access resource checks for API Keys with run as (#84431)
* Fix can access resource checks for API Keys with run as (#84277) This fixes two things for the "can access" authz check: * API Keys running as, have access to the resources created by the effective run as user * tokens created by API Keys (with the client credentials) have access to the API Key's resources In addition, this PR moves some of the authz plumbing code from the Async and Scroll services classes under the Security Context class (as a minor refactoring). * spotless * Merge fallout * AuthenticationTests Co-authored-by: Elastic Machine <[email protected]>
1 parent 16d2dde commit 2c1b34b

File tree

8 files changed

+501
-352
lines changed

8 files changed

+501
-352
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/async/AsyncTaskIndexService.java

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@
5757
import org.elasticsearch.xpack.core.search.action.AsyncSearchResponse;
5858
import org.elasticsearch.xpack.core.search.action.SearchStatusResponse;
5959
import org.elasticsearch.xpack.core.security.SecurityContext;
60-
import org.elasticsearch.xpack.core.security.authc.Authentication;
61-
import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer;
6260

6361
import java.io.FilterOutputStream;
6462
import java.io.IOException;
@@ -78,7 +76,6 @@
7876
import static org.elasticsearch.search.SearchService.MAX_ASYNC_SEARCH_RESPONSE_SIZE_SETTING;
7977
import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder;
8078
import static org.elasticsearch.xpack.core.ClientHelper.ASYNC_SEARCH_ORIGIN;
81-
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.AUTHENTICATION_KEY;
8279

8380
/**
8481
* A service that exposes the CRUD operations for the async task-specific index.
@@ -203,8 +200,8 @@ public Client getClient() {
203200
/**
204201
* Returns the authentication information, or null if the current context has no authentication info.
205202
**/
206-
public Authentication getAuthentication() {
207-
return securityContext.getAuthentication();
203+
public SecurityContext getSecurityContext() {
204+
return securityContext;
208205
}
209206

210207
/**
@@ -397,8 +394,7 @@ public <T extends AsyncTask> T getTaskAndCheckAuthentication(
397394
return null;
398395
}
399396
// Check authentication for the user
400-
final Authentication auth = securityContext.getAuthentication();
401-
if (ensureAuthenticatedUserIsSame(asyncTask.getOriginHeaders(), auth) == false) {
397+
if (false == securityContext.canIAccessResourcesCreatedWithHeaders(asyncTask.getOriginHeaders())) {
402398
throw new ResourceNotFoundException(asyncExecutionId.getEncoded() + " not found");
403399
}
404400
return asyncTask;
@@ -471,7 +467,7 @@ private R parseResponseFromIndex(
471467
@SuppressWarnings("unchecked")
472468
final Map<String, String> headers = (Map<String, String>) XContentParserUtils.parseFieldsValue(parser);
473469
// check the authentication of the current user against the user that initiated the async task
474-
if (checkAuthentication && ensureAuthenticatedUserIsSame(headers, securityContext.getAuthentication()) == false) {
470+
if (checkAuthentication && false == securityContext.canIAccessResourcesCreatedWithHeaders(headers)) {
475471
throw new ResourceNotFoundException(asyncExecutionId.getEncoded());
476472
}
477473
}
@@ -540,8 +536,7 @@ public <T extends AsyncTask, SR extends SearchStatusResponse> void retrieveStatu
540536
}
541537

542538
/**
543-
* Checks if the current user's authentication matches the original authentication stored
544-
* in the async search index.
539+
* Checks if the current user can access the async search result of the original user.
545540
**/
546541
void ensureAuthenticatedUserCanDeleteFromIndex(AsyncExecutionId executionId, ActionListener<Void> listener) {
547542
GetRequest internalGet = new GetRequest(index).preference(executionId.getEncoded())
@@ -556,32 +551,14 @@ void ensureAuthenticatedUserCanDeleteFromIndex(AsyncExecutionId executionId, Act
556551
// Check authentication for the user
557552
@SuppressWarnings("unchecked")
558553
Map<String, String> headers = (Map<String, String>) get.getSource().get(HEADERS_FIELD);
559-
if (ensureAuthenticatedUserIsSame(headers, securityContext.getAuthentication())) {
554+
if (securityContext.canIAccessResourcesCreatedWithHeaders(headers)) {
560555
listener.onResponse(null);
561556
} else {
562557
listener.onFailure(new ResourceNotFoundException(executionId.getEncoded()));
563558
}
564559
}, exc -> listener.onFailure(new ResourceNotFoundException(executionId.getEncoded()))));
565560
}
566561

567-
/**
568-
* Extracts the authentication from the original headers and checks that it matches
569-
* the current user. This function returns always <code>true</code> if the provided
570-
* <code>headers</code> do not contain any authentication.
571-
*/
572-
boolean ensureAuthenticatedUserIsSame(Map<String, String> originHeaders, Authentication current) throws IOException {
573-
if (originHeaders == null || originHeaders.containsKey(AUTHENTICATION_KEY) == false) {
574-
// no authorization attached to the original request
575-
return true;
576-
}
577-
if (current == null) {
578-
// origin is an authenticated user but current is not
579-
return false;
580-
}
581-
Authentication origin = AuthenticationContextSerializer.decode(originHeaders.get(AUTHENTICATION_KEY));
582-
return origin.canAccessResourcesOf(current);
583-
}
584-
585562
private void writeResponse(R response, OutputStream os) throws IOException {
586563
os = new FilterOutputStream(os) {
587564
@Override

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/async/DeleteAsyncResultsService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public void deleteResponse(DeleteAsyncResultRequest request, ActionListener<Ackn
5353
* delete async search submitted by another user.
5454
*/
5555
private void hasCancelTaskPrivilegeAsync(Consumer<Boolean> consumer) {
56-
final Authentication current = store.getAuthentication();
56+
final Authentication current = store.getSecurityContext().getAuthentication();
5757
if (current != null) {
5858
HasPrivilegesRequest req = new HasPrivilegesRequest();
5959
req.username(current.getUser().principal());

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

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import static org.elasticsearch.xpack.core.security.authc.Authentication.VERSION_API_KEY_ROLES_AS_BYTES;
3939
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_NAME;
4040
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_TYPE;
41+
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.AUTHENTICATION_KEY;
4142

4243
/**
4344
* A lightweight utility that can find the current user and authentication information for the local thread.
@@ -121,15 +122,6 @@ public void setUser(User user, Version version) {
121122
);
122123
}
123124

124-
/** Writes the authentication to the thread context */
125-
private void setAuthentication(Authentication authentication) {
126-
try {
127-
authentication.writeToContext(threadContext);
128-
} catch (IOException e) {
129-
throw new AssertionError("how can we have a IOException with a user we set", e);
130-
}
131-
}
132-
133125
/**
134126
* Runs the consumer in a new context as the provided user. The original context is provided to the consumer. When this method
135127
* returns, the original context is restored.
@@ -221,16 +213,61 @@ private Map<String, Object> rewriteMetadataForApiKeyRoleDescriptors(Version stre
221213
return metadata;
222214
}
223215

224-
private Map<String, Object> convertRoleDescriptorsBytesToMap(BytesReference roleDescriptorsBytes) {
216+
private static Map<String, Object> convertRoleDescriptorsBytesToMap(BytesReference roleDescriptorsBytes) {
225217
return XContentHelper.convertToMap(roleDescriptorsBytes, false, XContentType.JSON).v2();
226218
}
227219

228-
private BytesReference convertRoleDescriptorsMapToBytes(Map<String, Object> roleDescriptorsMap) {
220+
private static BytesReference convertRoleDescriptorsMapToBytes(Map<String, Object> roleDescriptorsMap) {
229221
try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) {
230222
builder.map(roleDescriptorsMap);
231223
return BytesReference.bytes(builder);
232224
} catch (IOException e) {
233225
throw new UncheckedIOException(e);
234226
}
235227
}
228+
229+
/**
230+
* Checks whether the user or API key of the passed in authentication can access the resources owned by the user
231+
* or API key of this authentication. The rules are as follows:
232+
* * True if the authentications are for the same API key (same API key ID)
233+
* * True if they are the same username from the same realm
234+
* - For file and native realm, same realm means the same realm type
235+
* - For all other realms, same realm means same realm type plus same realm name
236+
* * An user and its API key cannot access each other's resources
237+
* * An user and its token can access each other's resources
238+
* * Two API keys are never able to access each other's resources regardless of their ownership.
239+
*
240+
* This check is a best effort and it does not account for certain static and external changes.
241+
* See also <a href="https://www.elastic.co/guide/en/elasticsearch/reference/master/security-limitations.html">
242+
* security limitations</a>
243+
*/
244+
public boolean canIAccessResourcesCreatedBy(@Nullable Authentication resourceCreatorAuthentication) {
245+
if (resourceCreatorAuthentication == null) {
246+
// resource creation was not authenticated (security was disabled); anyone can access such resources
247+
return true;
248+
}
249+
final Authentication myAuthentication = getAuthentication();
250+
if (myAuthentication == null) {
251+
// unauthenticated users cannot access any resources created by authenticated users, even anonymously authenticated ones
252+
return false;
253+
}
254+
return myAuthentication.canAccessResourcesOf(resourceCreatorAuthentication);
255+
}
256+
257+
public boolean canIAccessResourcesCreatedWithHeaders(Map<String, String> resourceCreateRequestHeaders) throws IOException {
258+
Authentication resourceCreatorAuthentication = null;
259+
if (resourceCreateRequestHeaders != null && resourceCreateRequestHeaders.containsKey(AUTHENTICATION_KEY)) {
260+
resourceCreatorAuthentication = AuthenticationContextSerializer.decode(resourceCreateRequestHeaders.get(AUTHENTICATION_KEY));
261+
}
262+
return canIAccessResourcesCreatedBy(resourceCreatorAuthentication);
263+
}
264+
265+
/** Writes the authentication to the thread context */
266+
private void setAuthentication(Authentication authentication) {
267+
try {
268+
authentication.writeToContext(threadContext);
269+
} catch (IOException e) {
270+
throw new UncheckedIOException(e);
271+
}
272+
}
236273
}

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

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.Objects;
3333

3434
import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;
35+
import static org.elasticsearch.xpack.core.security.authc.Subject.Type.API_KEY;
3536

3637
// TODO(hub-cap) Clean this up after moving User over - This class can re-inherit its field AUTHENTICATION_KEY in AuthenticationField.
3738
// That interface can be removed
@@ -196,40 +197,55 @@ public void writeTo(StreamOutput out) throws IOException {
196197
* security limitations</a>
197198
*/
198199
public boolean canAccessResourcesOf(Authentication other) {
199-
if (isApiKey() && other.isApiKey()) {
200-
final boolean sameKeyId = getMetadata().get(AuthenticationField.API_KEY_ID_KEY)
201-
.equals(other.getMetadata().get(AuthenticationField.API_KEY_ID_KEY));
202-
if (sameKeyId) {
203-
assert getUser().principal().equals(getUser().principal())
204-
: "The same API key ID cannot be attributed to two different usernames";
205-
}
200+
// if we introduce new authentication types in the future, it is likely that we'll need to revisit this method
201+
assert EnumSet.of(
202+
Authentication.AuthenticationType.REALM,
203+
Authentication.AuthenticationType.API_KEY,
204+
Authentication.AuthenticationType.TOKEN,
205+
Authentication.AuthenticationType.ANONYMOUS,
206+
Authentication.AuthenticationType.INTERNAL
207+
).containsAll(EnumSet.of(getAuthenticationType(), other.getAuthenticationType()))
208+
: "cross AuthenticationType comparison for canAccessResourcesOf is not applicable for: "
209+
+ EnumSet.of(getAuthenticationType(), other.getAuthenticationType());
210+
final AuthenticationContext myAuthContext = AuthenticationContext.fromAuthentication(this);
211+
final AuthenticationContext creatorAuthContext = AuthenticationContext.fromAuthentication(other);
212+
if (API_KEY.equals(myAuthContext.getEffectiveSubject().getType())
213+
&& API_KEY.equals(creatorAuthContext.getEffectiveSubject().getType())) {
214+
final boolean sameKeyId = myAuthContext.getEffectiveSubject()
215+
.getMetadata()
216+
.get(AuthenticationField.API_KEY_ID_KEY)
217+
.equals(creatorAuthContext.getEffectiveSubject().getMetadata().get(AuthenticationField.API_KEY_ID_KEY));
218+
assert false == sameKeyId
219+
|| myAuthContext.getEffectiveSubject()
220+
.getUser()
221+
.principal()
222+
.equals(creatorAuthContext.getEffectiveSubject().getUser().principal())
223+
: "The same API key ID cannot be attributed to two different usernames";
206224
return sameKeyId;
207-
}
208-
209-
if (getAuthenticationType().equals(other.getAuthenticationType())
210-
|| (AuthenticationType.REALM == getAuthenticationType() && AuthenticationType.TOKEN == other.getAuthenticationType())
211-
|| (AuthenticationType.TOKEN == getAuthenticationType() && AuthenticationType.REALM == other.getAuthenticationType())) {
212-
if (false == getUser().principal().equals(other.getUser().principal())) {
213-
return false;
214-
}
215-
final RealmRef thisRealm = getSourceRealm();
216-
final RealmRef otherRealm = other.getSourceRealm();
217-
if (FileRealmSettings.TYPE.equals(thisRealm.getType()) || NativeRealmSettings.TYPE.equals(thisRealm.getType())) {
218-
return thisRealm.getType().equals(otherRealm.getType());
219-
}
220-
return thisRealm.getName().equals(otherRealm.getName()) && thisRealm.getType().equals(otherRealm.getType());
221-
} else {
222-
assert EnumSet.of(
223-
AuthenticationType.REALM,
224-
AuthenticationType.API_KEY,
225-
AuthenticationType.TOKEN,
226-
AuthenticationType.ANONYMOUS,
227-
AuthenticationType.INTERNAL
228-
).containsAll(EnumSet.of(getAuthenticationType(), other.getAuthenticationType()))
229-
: "cross AuthenticationType comparison for canAccessResourcesOf is not applicable for: "
230-
+ EnumSet.of(getAuthenticationType(), other.getAuthenticationType());
231-
return false;
232-
}
225+
} else if ((API_KEY.equals(myAuthContext.getEffectiveSubject().getType())
226+
&& false == API_KEY.equals(creatorAuthContext.getEffectiveSubject().getType()))
227+
|| (false == API_KEY.equals(myAuthContext.getEffectiveSubject().getType())
228+
&& API_KEY.equals(creatorAuthContext.getEffectiveSubject().getType()))) {
229+
// an API Key cannot access resources created by non-API Keys or vice-versa
230+
return false;
231+
} else {
232+
if (false == myAuthContext.getEffectiveSubject()
233+
.getUser()
234+
.principal()
235+
.equals(creatorAuthContext.getEffectiveSubject().getUser().principal())) {
236+
return false;
237+
}
238+
final Authentication.RealmRef myAuthRealm = myAuthContext.getEffectiveSubject().getRealm();
239+
final Authentication.RealmRef creatorAuthRealm = creatorAuthContext.getEffectiveSubject().getRealm();
240+
if (FileRealmSettings.TYPE.equals(myAuthRealm.getType()) || NativeRealmSettings.TYPE.equals(myAuthRealm.getType())) {
241+
// file and native realms can be renamed...
242+
// nonetheless, they are singleton realms, only one such realm of each type can exist
243+
return myAuthRealm.getType().equals(creatorAuthRealm.getType());
244+
} else {
245+
return myAuthRealm.getName().equals(creatorAuthRealm.getName())
246+
&& myAuthRealm.getType().equals(creatorAuthRealm.getType());
247+
}
248+
}
233249
}
234250

235251
@Override

0 commit comments

Comments
 (0)