mirror of
https://github.com/honeymoose/OpenSearch.git
synced 2025-03-01 00:19:11 +00:00
Ensure authz operation overrides transient authz headers (#61621)
AuthorizationService#authorize uses the thread context to carry the result of the authorisation as transient headers. The listener argument to the `authorize` method must necessarily observe the header values. This PR makes it so that the authorisation transient headers (`_indices_permissions` and `_authz_info`, but NOT `_originating_action_name`) of the child action override the ones of the parent action. Co-authored-by: Tim Vernum tim@adjective.org
This commit is contained in:
parent
76f56c1264
commit
aeed1c05b0
@ -36,6 +36,7 @@ import org.elasticsearch.tasks.Task;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
@ -189,12 +190,41 @@ public final class ThreadContext implements Writeable {
|
||||
* @param preserveResponseHeaders if set to <code>true</code> the response headers of the restore thread will be preserved.
|
||||
*/
|
||||
public StoredContext newStoredContext(boolean preserveResponseHeaders) {
|
||||
final ThreadContextStruct context = threadLocal.get();
|
||||
return () -> {
|
||||
if (preserveResponseHeaders && threadLocal.get() != context) {
|
||||
threadLocal.set(context.putResponseHeaders(threadLocal.get().responseHeaders));
|
||||
return newStoredContext(preserveResponseHeaders, Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Just like {@link #stashContext()} but no default context is set. Instead, the {@code transientHeadersToClear} argument can be used
|
||||
* to clear specific transient headers in the new context. All headers (with the possible exception of {@code responseHeaders}) are
|
||||
* restored by closing the returned {@link StoredContext}.
|
||||
*
|
||||
* @param preserveResponseHeaders if set to <code>true</code> the response headers of the restore thread will be preserved.
|
||||
*/
|
||||
public StoredContext newStoredContext(boolean preserveResponseHeaders, Collection<String> transientHeadersToClear) {
|
||||
final ThreadContextStruct originalContext = threadLocal.get();
|
||||
// clear specific transient headers from the current context
|
||||
Map<String, Object> newTransientHeaders = null;
|
||||
for (String transientHeaderToClear : transientHeadersToClear) {
|
||||
if (originalContext.transientHeaders.containsKey(transientHeaderToClear)) {
|
||||
if (newTransientHeaders == null) {
|
||||
newTransientHeaders = new HashMap<>(originalContext.transientHeaders);
|
||||
}
|
||||
newTransientHeaders.remove(transientHeaderToClear);
|
||||
}
|
||||
}
|
||||
if (newTransientHeaders != null) {
|
||||
ThreadContextStruct threadContextStruct = new ThreadContextStruct(originalContext.requestHeaders,
|
||||
originalContext.responseHeaders, newTransientHeaders, originalContext.isSystemContext,
|
||||
originalContext.warningHeadersSize);
|
||||
threadLocal.set(threadContextStruct);
|
||||
}
|
||||
// this is the context when this method returns
|
||||
final ThreadContextStruct newContext = threadLocal.get();
|
||||
return () -> {
|
||||
if (preserveResponseHeaders && threadLocal.get() != newContext) {
|
||||
threadLocal.set(originalContext.putResponseHeaders(threadLocal.get().responseHeaders));
|
||||
} else {
|
||||
threadLocal.set(context);
|
||||
threadLocal.set(originalContext);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -508,7 +538,7 @@ public final class ThreadContext implements Writeable {
|
||||
return new ThreadContextStruct(newRequestHeaders, responseHeaders, transientHeaders, isSystemContext);
|
||||
}
|
||||
|
||||
private static void putSingleHeader(String key, String value, Map<String, String> newHeaders) {
|
||||
private static <T> void putSingleHeader(String key, T value, Map<String, T> newHeaders) {
|
||||
if (newHeaders.putIfAbsent(key, value) != null) {
|
||||
throw new IllegalArgumentException("value for key [" + key + "] already present");
|
||||
}
|
||||
@ -596,12 +626,9 @@ public final class ThreadContext implements Writeable {
|
||||
return new ThreadContextStruct(requestHeaders, newResponseHeaders, transientHeaders, isSystemContext, newWarningHeaderSize);
|
||||
}
|
||||
|
||||
|
||||
private ThreadContextStruct putTransient(String key, Object value) {
|
||||
Map<String, Object> newTransient = new HashMap<>(this.transientHeaders);
|
||||
if (newTransient.putIfAbsent(key, value) != null) {
|
||||
throw new IllegalArgumentException("value for key [" + key + "] already present");
|
||||
}
|
||||
putSingleHeader(key, value, newTransient);
|
||||
return new ThreadContextStruct(requestHeaders, responseHeaders, newTransient, isSystemContext);
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -56,6 +57,78 @@ public class ThreadContextTests extends ESTestCase {
|
||||
assertEquals("1", threadContext.getHeader("default"));
|
||||
}
|
||||
|
||||
public void testNewContextWithClearedTransients() {
|
||||
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
|
||||
threadContext.putTransient("foo", "bar");
|
||||
threadContext.putTransient("bar", "baz");
|
||||
threadContext.putHeader("foo", "bar");
|
||||
threadContext.putHeader("baz", "bar");
|
||||
threadContext.addResponseHeader("foo", "bar");
|
||||
threadContext.addResponseHeader("bar", "qux");
|
||||
|
||||
// this is missing or null
|
||||
if (randomBoolean()) {
|
||||
threadContext.putTransient("acme", null);
|
||||
}
|
||||
|
||||
// foo is the only existing transient header that is cleared
|
||||
try (ThreadContext.StoredContext stashed = threadContext.newStoredContext(false, randomFrom(Arrays.asList("foo", "foo"),
|
||||
Arrays.asList("foo"), Arrays.asList("foo", "acme")))) {
|
||||
// only the requested transient header is cleared
|
||||
assertNull(threadContext.getTransient("foo"));
|
||||
// missing header is still missing
|
||||
assertNull(threadContext.getTransient("acme"));
|
||||
// other headers are preserved
|
||||
assertEquals("baz", threadContext.getTransient("bar"));
|
||||
assertEquals("bar", threadContext.getHeader("foo"));
|
||||
assertEquals("bar", threadContext.getHeader("baz"));
|
||||
assertEquals("bar", threadContext.getResponseHeaders().get("foo").get(0));
|
||||
assertEquals("qux", threadContext.getResponseHeaders().get("bar").get(0));
|
||||
|
||||
// try override stashed header
|
||||
threadContext.putTransient("foo", "acme");
|
||||
assertEquals("acme", threadContext.getTransient("foo"));
|
||||
// add new headers
|
||||
threadContext.putTransient("baz", "bar");
|
||||
threadContext.putHeader("bar", "baz");
|
||||
threadContext.addResponseHeader("baz", "bar");
|
||||
threadContext.addResponseHeader("foo", "baz");
|
||||
}
|
||||
|
||||
// original is restored (it is not overridden)
|
||||
assertEquals("bar", threadContext.getTransient("foo"));
|
||||
// headers added inside the stash are NOT preserved
|
||||
assertNull(threadContext.getTransient("baz"));
|
||||
assertNull(threadContext.getHeader("bar"));
|
||||
assertNull(threadContext.getResponseHeaders().get("baz"));
|
||||
// original headers are restored
|
||||
assertEquals("bar", threadContext.getHeader("foo"));
|
||||
assertEquals("bar", threadContext.getHeader("baz"));
|
||||
assertEquals("bar", threadContext.getResponseHeaders().get("foo").get(0));
|
||||
assertEquals(1, threadContext.getResponseHeaders().get("foo").size());
|
||||
assertEquals("qux", threadContext.getResponseHeaders().get("bar").get(0));
|
||||
|
||||
// test stashed missing header stays missing
|
||||
try (ThreadContext.StoredContext stashed = threadContext.newStoredContext(randomBoolean(), randomFrom(Arrays.asList("acme", "acme"),
|
||||
Arrays.asList("acme")))) {
|
||||
assertNull(threadContext.getTransient("acme"));
|
||||
threadContext.putTransient("acme", "foo");
|
||||
}
|
||||
assertNull(threadContext.getTransient("acme"));
|
||||
|
||||
// test preserved response headers
|
||||
try (ThreadContext.StoredContext stashed = threadContext.newStoredContext(true, randomFrom(Arrays.asList("foo", "foo"),
|
||||
Arrays.asList("foo"), Arrays.asList("foo", "acme")))) {
|
||||
threadContext.addResponseHeader("baz", "bar");
|
||||
threadContext.addResponseHeader("foo", "baz");
|
||||
}
|
||||
assertEquals("bar", threadContext.getResponseHeaders().get("foo").get(0));
|
||||
assertEquals("baz", threadContext.getResponseHeaders().get("foo").get(1));
|
||||
assertEquals(2, threadContext.getResponseHeaders().get("foo").size());
|
||||
assertEquals("bar", threadContext.getResponseHeaders().get("baz").get(0));
|
||||
assertEquals(1, threadContext.getResponseHeaders().get("baz").size());
|
||||
}
|
||||
|
||||
public void testStashWithOrigin() {
|
||||
final String origin = randomAlphaOfLengthBetween(4, 16);
|
||||
final ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
|
||||
|
@ -5,8 +5,18 @@
|
||||
*/
|
||||
package org.elasticsearch.xpack.core.security.authz;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
public final class AuthorizationServiceField {
|
||||
|
||||
public static final String INDICES_PERMISSIONS_KEY = "_indices_permissions";
|
||||
public static final String ORIGINATING_ACTION_KEY = "_originating_action_name";
|
||||
public static final String AUTHORIZATION_INFO_KEY = "_authz_info";
|
||||
|
||||
// Most often, transient authorisation headers are scoped (i.e. set, read and cleared) for the authorisation and execution
|
||||
// of individual actions (i.e. there is a different scope between the parent and the child actions)
|
||||
public static final Collection<String> ACTION_SCOPE_AUTHORIZATION_KEYS = Arrays.asList(INDICES_PERMISSIONS_KEY, AUTHORIZATION_INFO_KEY);
|
||||
|
||||
private AuthorizationServiceField() {}
|
||||
}
|
||||
|
@ -40,8 +40,8 @@ import org.mockito.Mockito;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME;
|
||||
import static org.elasticsearch.xpack.security.authz.AuthorizationService.AUTHORIZATION_INFO_KEY;
|
||||
import static org.elasticsearch.xpack.security.authz.AuthorizationService.ORIGINATING_ACTION_KEY;
|
||||
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.AUTHORIZATION_INFO_KEY;
|
||||
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.ORIGINATING_ACTION_KEY;
|
||||
import static org.elasticsearch.xpack.security.authz.AuthorizationServiceTests.authzInfoRoles;
|
||||
import static org.elasticsearch.xpack.security.authz.SecuritySearchOperationListener.ensureAuthenticatedUserIsSame;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
@ -54,7 +54,6 @@ import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.Authoriza
|
||||
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.EmptyAuthorizationInfo;
|
||||
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.IndexAuthorizationResult;
|
||||
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.RequestInfo;
|
||||
import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField;
|
||||
import org.elasticsearch.xpack.core.security.authz.ResolvedIndices;
|
||||
import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl;
|
||||
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor;
|
||||
@ -89,14 +88,16 @@ import java.util.function.Consumer;
|
||||
|
||||
import static org.elasticsearch.action.support.ContextPreservingActionListener.wrapPreservingContext;
|
||||
import static org.elasticsearch.xpack.core.security.SecurityField.setting;
|
||||
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.ACTION_SCOPE_AUTHORIZATION_KEYS;
|
||||
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.AUTHORIZATION_INFO_KEY;
|
||||
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.INDICES_PERMISSIONS_KEY;
|
||||
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.ORIGINATING_ACTION_KEY;
|
||||
import static org.elasticsearch.xpack.core.security.support.Exceptions.authorizationError;
|
||||
import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME;
|
||||
|
||||
public class AuthorizationService {
|
||||
public static final Setting<Boolean> ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING =
|
||||
Setting.boolSetting(setting("authc.anonymous.authz_exception"), true, Property.NodeScope);
|
||||
public static final String ORIGINATING_ACTION_KEY = "_originating_action_name";
|
||||
public static final String AUTHORIZATION_INFO_KEY = "_authz_info";
|
||||
private static final AuthorizationInfo SYSTEM_AUTHZ_INFO =
|
||||
() -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, new String[] { SystemUser.ROLE_NAME });
|
||||
private static final String IMPLIED_INDEX_ACTION = IndexAction.NAME + ":op_type/index";
|
||||
@ -167,39 +168,51 @@ public class AuthorizationService {
|
||||
*/
|
||||
public void authorize(final Authentication authentication, final String action, final TransportRequest originalRequest,
|
||||
final ActionListener<Void> listener) throws ElasticsearchSecurityException {
|
||||
// prior to doing any authorization lets set the originating action in the context only
|
||||
putTransientIfNonExisting(ORIGINATING_ACTION_KEY, action);
|
||||
/* authorization fills in certain transient headers, which must be observed in the listener (action handler execution)
|
||||
* as well, but which must not bleed across different action context (eg parent-child action contexts).
|
||||
* <p>
|
||||
* Therefore we begin by clearing the existing ones up, as they might already be set during the authorization of a
|
||||
* previous parent action that ran under the same thread context (also on the same node).
|
||||
* When the returned {@code StoredContext} is closed, ALL the original headers are restored.
|
||||
*/
|
||||
try (ThreadContext.StoredContext ignore = threadContext.newStoredContext(false,
|
||||
ACTION_SCOPE_AUTHORIZATION_KEYS)) { // this does not clear {@code AuthorizationServiceField.ORIGINATING_ACTION_KEY}
|
||||
// prior to doing any authorization lets set the originating action in the thread context
|
||||
// the originating action is the current action if no originating action has yet been set in the current thread context
|
||||
// if there is already an original action, that stays put (eg. the current action is a child action)
|
||||
putTransientIfNonExisting(ORIGINATING_ACTION_KEY, action);
|
||||
|
||||
String auditId = AuditUtil.extractRequestId(threadContext);
|
||||
if (auditId == null) {
|
||||
// We would like to assert that there is an existing request-id, but if this is a system action, then that might not be
|
||||
// true because the request-id is generated during authentication
|
||||
if (isInternalUser(authentication.getUser()) != false) {
|
||||
auditId = AuditUtil.getOrGenerateRequestId(threadContext);
|
||||
} else {
|
||||
auditTrailService.get().tamperedRequest(null, authentication, action, originalRequest);
|
||||
final String message = "Attempt to authorize action [" + action + "] for [" + authentication.getUser().principal()
|
||||
+ "] without an existing request-id";
|
||||
assert false : message;
|
||||
listener.onFailure(new ElasticsearchSecurityException(message));
|
||||
String auditId = AuditUtil.extractRequestId(threadContext);
|
||||
if (auditId == null) {
|
||||
// We would like to assert that there is an existing request-id, but if this is a system action, then that might not be
|
||||
// true because the request-id is generated during authentication
|
||||
if (isInternalUser(authentication.getUser()) != false) {
|
||||
auditId = AuditUtil.getOrGenerateRequestId(threadContext);
|
||||
} else {
|
||||
auditTrailService.get().tamperedRequest(null, authentication, action, originalRequest);
|
||||
final String message = "Attempt to authorize action [" + action + "] for [" + authentication.getUser().principal()
|
||||
+ "] without an existing request-id";
|
||||
assert false : message;
|
||||
listener.onFailure(new ElasticsearchSecurityException(message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sometimes a request might be wrapped within another, which is the case for proxied
|
||||
// requests and concrete shard requests
|
||||
final TransportRequest unwrappedRequest = maybeUnwrapRequest(authentication, originalRequest, action, auditId);
|
||||
if (SystemUser.is(authentication.getUser())) {
|
||||
// this never goes async so no need to wrap the listener
|
||||
authorizeSystemUser(authentication, action, auditId, unwrappedRequest, listener);
|
||||
} else {
|
||||
final String finalAuditId = auditId;
|
||||
final RequestInfo requestInfo = new RequestInfo(authentication, unwrappedRequest, action);
|
||||
final ActionListener<AuthorizationInfo> authzInfoListener = wrapPreservingContext(ActionListener.wrap(
|
||||
authorizationInfo -> {
|
||||
putTransientIfNonExisting(AUTHORIZATION_INFO_KEY, authorizationInfo);
|
||||
maybeAuthorizeRunAs(requestInfo, finalAuditId, authorizationInfo, listener);
|
||||
}, listener::onFailure), threadContext);
|
||||
getAuthorizationEngine(authentication).resolveAuthorizationInfo(requestInfo, authzInfoListener);
|
||||
// sometimes a request might be wrapped within another, which is the case for proxied
|
||||
// requests and concrete shard requests
|
||||
final TransportRequest unwrappedRequest = maybeUnwrapRequest(authentication, originalRequest, action, auditId);
|
||||
if (SystemUser.is(authentication.getUser())) {
|
||||
// this never goes async so no need to wrap the listener
|
||||
authorizeSystemUser(authentication, action, auditId, unwrappedRequest, listener);
|
||||
} else {
|
||||
final String finalAuditId = auditId;
|
||||
final RequestInfo requestInfo = new RequestInfo(authentication, unwrappedRequest, action);
|
||||
final ActionListener<AuthorizationInfo> authzInfoListener = wrapPreservingContext(ActionListener.wrap(
|
||||
authorizationInfo -> {
|
||||
threadContext.putTransient(AUTHORIZATION_INFO_KEY, authorizationInfo);
|
||||
maybeAuthorizeRunAs(requestInfo, finalAuditId, authorizationInfo, listener);
|
||||
}, listener::onFailure), threadContext);
|
||||
getAuthorizationEngine(authentication).resolveAuthorizationInfo(requestInfo, authzInfoListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -246,7 +259,7 @@ public class AuthorizationService {
|
||||
if (ClusterPrivilegeResolver.isClusterAction(action)) {
|
||||
final ActionListener<AuthorizationResult> clusterAuthzListener =
|
||||
wrapPreservingContext(new AuthorizationResultListener<>(result -> {
|
||||
putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, IndicesAccessControl.ALLOW_ALL);
|
||||
threadContext.putTransient(INDICES_PERMISSIONS_KEY, IndicesAccessControl.ALLOW_ALL);
|
||||
listener.onResponse(null);
|
||||
}, listener::onFailure, requestInfo, requestId, authzInfo), threadContext);
|
||||
authzEngine.authorizeClusterAction(requestInfo, authzInfo, clusterAuthzListener);
|
||||
@ -290,8 +303,7 @@ public class AuthorizationService {
|
||||
final TransportRequest request = requestInfo.getRequest();
|
||||
final String action = requestInfo.getAction();
|
||||
if (result.getIndicesAccessControl() != null) {
|
||||
putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY,
|
||||
result.getIndicesAccessControl());
|
||||
threadContext.putTransient(INDICES_PERMISSIONS_KEY, result.getIndicesAccessControl());
|
||||
}
|
||||
//if we are creating an index we need to authorize potential aliases created at the same time
|
||||
if (IndexPrivilege.CREATE_INDEX_MATCHER.test(action)) {
|
||||
@ -383,8 +395,8 @@ public class AuthorizationService {
|
||||
final TransportRequest request, final ActionListener<Void> listener) {
|
||||
final AuditTrail auditTrail = auditTrailService.get();
|
||||
if (SystemUser.isAuthorized(action)) {
|
||||
putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, IndicesAccessControl.ALLOW_ALL);
|
||||
putTransientIfNonExisting(AUTHORIZATION_INFO_KEY, SYSTEM_AUTHZ_INFO);
|
||||
threadContext.putTransient(INDICES_PERMISSIONS_KEY, IndicesAccessControl.ALLOW_ALL);
|
||||
threadContext.putTransient(AUTHORIZATION_INFO_KEY, SYSTEM_AUTHZ_INFO);
|
||||
auditTrail.accessGranted(requestId, authentication, action, request, SYSTEM_AUTHZ_INFO);
|
||||
listener.onResponse(null);
|
||||
} else {
|
||||
|
@ -11,6 +11,7 @@ import org.elasticsearch.xpack.core.ClientHelper;
|
||||
import org.elasticsearch.xpack.core.security.SecurityContext;
|
||||
import org.elasticsearch.xpack.core.security.authc.Authentication;
|
||||
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
|
||||
import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField;
|
||||
import org.elasticsearch.xpack.core.security.support.Automatons;
|
||||
import org.elasticsearch.xpack.core.security.user.AsyncSearchUser;
|
||||
import org.elasticsearch.xpack.core.security.user.XPackSecurityUser;
|
||||
@ -73,7 +74,7 @@ public final class AuthorizationUtils {
|
||||
// we have a internal action being executed by a user other than the system user, lets verify that there is a
|
||||
// originating action that is not a internal action. We verify that there must be a originating action as an
|
||||
// internal action should never be called by user code from a client
|
||||
final String originatingAction = threadContext.getTransient(AuthorizationService.ORIGINATING_ACTION_KEY);
|
||||
final String originatingAction = threadContext.getTransient(AuthorizationServiceField.ORIGINATING_ACTION_KEY);
|
||||
if (originatingAction != null && isInternalAction(originatingAction) == false) {
|
||||
return true;
|
||||
}
|
||||
|
@ -24,8 +24,8 @@ import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessCo
|
||||
import org.elasticsearch.xpack.security.audit.AuditTrailService;
|
||||
import org.elasticsearch.xpack.security.audit.AuditUtil;
|
||||
|
||||
import static org.elasticsearch.xpack.security.authz.AuthorizationService.AUTHORIZATION_INFO_KEY;
|
||||
import static org.elasticsearch.xpack.security.authz.AuthorizationService.ORIGINATING_ACTION_KEY;
|
||||
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.AUTHORIZATION_INFO_KEY;
|
||||
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.ORIGINATING_ACTION_KEY;
|
||||
|
||||
/**
|
||||
* A {@link SearchOperationListener} that is used to provide authorization for scroll requests.
|
||||
|
@ -33,6 +33,8 @@ import org.elasticsearch.xpack.core.security.SecurityContext;
|
||||
import org.elasticsearch.xpack.core.security.authc.Authentication;
|
||||
import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef;
|
||||
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
|
||||
import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField;
|
||||
import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl;
|
||||
import org.elasticsearch.xpack.core.security.user.SystemUser;
|
||||
import org.elasticsearch.xpack.core.security.user.User;
|
||||
import org.elasticsearch.xpack.security.authc.AuthenticationService;
|
||||
@ -41,7 +43,9 @@ import org.junit.Before;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.INDICES_PERMISSIONS_KEY;
|
||||
import static org.hamcrest.Matchers.arrayWithSize;
|
||||
import static org.hamcrest.Matchers.sameInstance;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Matchers.isA;
|
||||
@ -94,21 +98,8 @@ public class SecurityActionFilterTests extends ESTestCase {
|
||||
Task task = mock(Task.class);
|
||||
User user = new User("username", "r1", "r2");
|
||||
Authentication authentication = new Authentication(user, new RealmRef("test", "test", "foo"), null);
|
||||
doAnswer(i -> {
|
||||
final Object[] args = i.getArguments();
|
||||
assertThat(args, arrayWithSize(4));
|
||||
ActionListener callback = (ActionListener) args[args.length - 1];
|
||||
callback.onResponse(authentication);
|
||||
return Void.TYPE;
|
||||
}).when(authcService).authenticate(eq("_action"), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class));
|
||||
doAnswer(i -> {
|
||||
final Object[] args = i.getArguments();
|
||||
assertThat(args, arrayWithSize(4));
|
||||
ActionListener callback = (ActionListener) args[args.length - 1];
|
||||
callback.onResponse(null);
|
||||
return Void.TYPE;
|
||||
}).when(authzService)
|
||||
.authorize(any(Authentication.class), any(String.class), any(TransportRequest.class), any(ActionListener.class));
|
||||
mockAuthentication(request, authentication);
|
||||
mockAuthorize();
|
||||
filter.apply(task, "_action", request, listener, chain);
|
||||
verify(authzService).authorize(eq(authentication), eq("_action"), eq(request), any(ActionListener.class));
|
||||
verify(chain).proceed(eq(task), eq("_action"), eq(request), isA(ContextPreservingActionListener.class));
|
||||
@ -121,28 +112,15 @@ public class SecurityActionFilterTests extends ESTestCase {
|
||||
Task task = mock(Task.class);
|
||||
User user = new User("username", "r1", "r2");
|
||||
Authentication authentication = new Authentication(user, new RealmRef("test", "test", "foo"), null);
|
||||
doAnswer(i -> {
|
||||
final Object[] args = i.getArguments();
|
||||
assertThat(args, arrayWithSize(4));
|
||||
ActionListener callback = (ActionListener) args[args.length - 1];
|
||||
assertNull(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY));
|
||||
threadContext.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication);
|
||||
callback.onResponse(authentication);
|
||||
return Void.TYPE;
|
||||
}).when(authcService).authenticate(eq("_action"), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class));
|
||||
doAnswer(i -> {
|
||||
final Object[] args = i.getArguments();
|
||||
assertThat(args, arrayWithSize(4));
|
||||
ActionListener callback = (ActionListener) args[args.length - 1];
|
||||
callback.onResponse(null);
|
||||
return Void.TYPE;
|
||||
}).when(authzService)
|
||||
.authorize(any(Authentication.class), any(String.class), any(TransportRequest.class), any(ActionListener.class));
|
||||
mockAuthentication(request, authentication);
|
||||
mockAuthorize();
|
||||
assertNull(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY));
|
||||
assertNull(threadContext.getTransient(INDICES_PERMISSIONS_KEY));
|
||||
|
||||
filter.apply(task, "_action", request, listener, chain);
|
||||
|
||||
assertNull(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY));
|
||||
assertNull(threadContext.getTransient(INDICES_PERMISSIONS_KEY));
|
||||
verify(authzService).authorize(eq(authentication), eq("_action"), eq(request), any(ActionListener.class));
|
||||
verify(chain).proceed(eq(task), eq("_action"), eq(request), isA(ContextPreservingActionListener.class));
|
||||
}
|
||||
@ -153,16 +131,22 @@ public class SecurityActionFilterTests extends ESTestCase {
|
||||
User user = new User("username", "r1", "r2");
|
||||
Authentication authentication = new Authentication(user, new RealmRef("test", "test", "foo"), null);
|
||||
SetOnce<Authentication> authenticationSetOnce = new SetOnce<>();
|
||||
SetOnce<IndicesAccessControl> accessControlSetOnce = new SetOnce<>();
|
||||
ActionFilterChain chain = (task, action, request1, listener1) -> {
|
||||
authenticationSetOnce.set(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY));
|
||||
accessControlSetOnce.set(threadContext.getTransient(INDICES_PERMISSIONS_KEY));
|
||||
};
|
||||
Task task = mock(Task.class);
|
||||
final boolean hasExistingAuthentication = randomBoolean();
|
||||
final boolean hasExistingAccessControl = randomBoolean();
|
||||
final String action = "internal:foo";
|
||||
if (hasExistingAuthentication) {
|
||||
threadContext.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication);
|
||||
threadContext.putHeader(AuthenticationField.AUTHENTICATION_KEY, "foo");
|
||||
threadContext.putTransient(AuthorizationService.ORIGINATING_ACTION_KEY, "indices:foo");
|
||||
threadContext.putTransient(AuthorizationServiceField.ORIGINATING_ACTION_KEY, "indices:foo");
|
||||
if (hasExistingAccessControl) {
|
||||
threadContext.putTransient(INDICES_PERMISSIONS_KEY, IndicesAccessControl.ALLOW_NO_INDICES);
|
||||
}
|
||||
} else {
|
||||
assertNull(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY));
|
||||
}
|
||||
@ -173,23 +157,23 @@ public class SecurityActionFilterTests extends ESTestCase {
|
||||
callback.onResponse(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY));
|
||||
return Void.TYPE;
|
||||
}).when(authcService).authenticate(eq(action), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class));
|
||||
doAnswer((i) -> {
|
||||
ActionListener<Void> callback = (ActionListener<Void>) i.getArguments()[3];
|
||||
callback.onResponse(null);
|
||||
return Void.TYPE;
|
||||
}).when(authzService)
|
||||
.authorize(any(Authentication.class), any(String.class), any(TransportRequest.class), any(ActionListener.class));
|
||||
IndicesAccessControl authzAccessControl = mock(IndicesAccessControl.class);
|
||||
mockAuthorize(authzAccessControl);
|
||||
|
||||
filter.apply(task, action, request, listener, chain);
|
||||
|
||||
if (hasExistingAuthentication) {
|
||||
assertEquals(authentication, threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY));
|
||||
if (hasExistingAccessControl) {
|
||||
assertThat(threadContext.getTransient(INDICES_PERMISSIONS_KEY), sameInstance(IndicesAccessControl.ALLOW_NO_INDICES));
|
||||
}
|
||||
} else {
|
||||
assertNull(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY));
|
||||
}
|
||||
assertNotNull(authenticationSetOnce.get());
|
||||
assertNotEquals(authentication, authenticationSetOnce.get());
|
||||
assertEquals(SystemUser.INSTANCE, authenticationSetOnce.get().getUser());
|
||||
assertThat(accessControlSetOnce.get(), sameInstance(authzAccessControl));
|
||||
}
|
||||
|
||||
public void testApplyDestructiveOperations() throws Exception {
|
||||
@ -257,4 +241,34 @@ public class SecurityActionFilterTests extends ESTestCase {
|
||||
verifyZeroInteractions(authzService);
|
||||
verify(chain).proceed(eq(task), eq("_action"), eq(request), eq(listener));
|
||||
}
|
||||
|
||||
private void mockAuthentication(ActionRequest request, Authentication authentication) {
|
||||
doAnswer(i -> {
|
||||
final Object[] args = i.getArguments();
|
||||
assertThat(args, arrayWithSize(4));
|
||||
ActionListener callback = (ActionListener) args[args.length - 1];
|
||||
assertNull(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY));
|
||||
threadContext.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication);
|
||||
callback.onResponse(authentication);
|
||||
return Void.TYPE;
|
||||
}).when(authcService).authenticate(eq("_action"), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class));
|
||||
}
|
||||
|
||||
private void mockAuthorize() {
|
||||
mockAuthorize(IndicesAccessControl.ALLOW_NO_INDICES);
|
||||
}
|
||||
|
||||
private void mockAuthorize(IndicesAccessControl indicesAccessControl) {
|
||||
doAnswer(i -> {
|
||||
final Object[] args = i.getArguments();
|
||||
assertThat(args, arrayWithSize(4));
|
||||
ActionListener callback = (ActionListener) args[args.length - 1];
|
||||
assertNull(threadContext.getTransient(INDICES_PERMISSIONS_KEY));
|
||||
threadContext.putTransient(INDICES_PERMISSIONS_KEY, indicesAccessControl);
|
||||
callback.onResponse(null);
|
||||
return Void.TYPE;
|
||||
}).when(authzService)
|
||||
.authorize(any(Authentication.class), any(String.class), any(TransportRequest.class), any(ActionListener.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -80,6 +80,7 @@ import org.elasticsearch.cluster.node.DiscoveryNode;
|
||||
import org.elasticsearch.cluster.service.ClusterService;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.TriFunction;
|
||||
import org.elasticsearch.common.UUIDs;
|
||||
import org.elasticsearch.common.collect.Tuple;
|
||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
import org.elasticsearch.common.settings.ClusterSettings;
|
||||
@ -161,16 +162,22 @@ import static java.util.Arrays.asList;
|
||||
import static org.elasticsearch.test.SecurityTestsUtils.assertAuthenticationException;
|
||||
import static org.elasticsearch.test.SecurityTestsUtils.assertThrowsAuthorizationException;
|
||||
import static org.elasticsearch.test.SecurityTestsUtils.assertThrowsAuthorizationExceptionRunAs;
|
||||
import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME;
|
||||
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.AUTHORIZATION_INFO_KEY;
|
||||
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.INDICES_PERMISSIONS_KEY;
|
||||
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.ORIGINATING_ACTION_KEY;
|
||||
import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.INTERNAL_SECURITY_MAIN_INDEX_7;
|
||||
import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.SECURITY_MAIN_ALIAS;
|
||||
import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME;
|
||||
import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.endsWith;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.hamcrest.Matchers.sameInstance;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.eq;
|
||||
@ -252,9 +259,70 @@ public class AuthorizationServiceTests extends ESTestCase {
|
||||
}
|
||||
|
||||
private void authorize(Authentication authentication, String action, TransportRequest request) {
|
||||
PlainActionFuture<Void> future = new PlainActionFuture<>();
|
||||
authorizationService.authorize(authentication, action, request, future);
|
||||
future.actionGet();
|
||||
PlainActionFuture<Object> done = new PlainActionFuture<>();
|
||||
PlainActionFuture<IndicesAccessControl> indicesPermissions = new PlainActionFuture<>();
|
||||
PlainActionFuture<Object> originatingAction = new PlainActionFuture<>();
|
||||
PlainActionFuture<Object> authorizationInfo = new PlainActionFuture<>();
|
||||
String someRandomHeader = "test_" + UUIDs.randomBase64UUID();
|
||||
Object someRandomHeaderValue = mock(Object.class);
|
||||
threadContext.putTransient(someRandomHeader, someRandomHeaderValue);
|
||||
// the thread context before authorization could contain any of the transient headers
|
||||
IndicesAccessControl mockAccessControlHeader = threadContext.getTransient(INDICES_PERMISSIONS_KEY);
|
||||
if (mockAccessControlHeader == null && randomBoolean()) {
|
||||
mockAccessControlHeader = mock(IndicesAccessControl.class);
|
||||
threadContext.putTransient(INDICES_PERMISSIONS_KEY, mockAccessControlHeader);
|
||||
}
|
||||
String originatingActionHeader = threadContext.getTransient(ORIGINATING_ACTION_KEY);
|
||||
if (originatingActionHeader == null && randomBoolean()) {
|
||||
originatingActionHeader = randomAlphaOfLength(8);
|
||||
threadContext.putTransient(ORIGINATING_ACTION_KEY, originatingActionHeader);
|
||||
}
|
||||
AuthorizationInfo authorizationInfoHeader = threadContext.getTransient(AUTHORIZATION_INFO_KEY);
|
||||
if (authorizationInfoHeader == null && randomBoolean()) {
|
||||
authorizationInfoHeader = mock(AuthorizationInfo.class);
|
||||
threadContext.putTransient(AUTHORIZATION_INFO_KEY, authorizationInfoHeader);
|
||||
}
|
||||
ActionListener<Void> listener = ActionListener.wrap(response -> {
|
||||
// extract the authorization transient headers from the thread context of the action
|
||||
// that has been authorized
|
||||
originatingAction.onResponse(threadContext.getTransient(ORIGINATING_ACTION_KEY));
|
||||
authorizationInfo.onResponse(threadContext.getTransient(AUTHORIZATION_INFO_KEY));
|
||||
indicesPermissions.onResponse(threadContext.getTransient(INDICES_PERMISSIONS_KEY));
|
||||
done.onResponse(threadContext.getTransient(someRandomHeader));
|
||||
}, e -> {
|
||||
done.onFailure(e);
|
||||
});
|
||||
authorizationService.authorize(authentication, action, request, listener);
|
||||
Object someRandonHeaderValueInListener = done.actionGet();
|
||||
assertThat(someRandonHeaderValueInListener, sameInstance(someRandomHeaderValue));
|
||||
assertThat(threadContext.getTransient(someRandomHeader), sameInstance(someRandomHeaderValue));
|
||||
// authorization restores any previously existing transient headers
|
||||
if (mockAccessControlHeader != null) {
|
||||
assertThat(threadContext.getTransient(INDICES_PERMISSIONS_KEY), sameInstance(mockAccessControlHeader));
|
||||
} else {
|
||||
assertThat(threadContext.getTransient(INDICES_PERMISSIONS_KEY), nullValue());
|
||||
}
|
||||
if (originatingActionHeader != null) {
|
||||
assertThat(threadContext.getTransient(ORIGINATING_ACTION_KEY), sameInstance(originatingActionHeader));
|
||||
} else {
|
||||
assertThat(threadContext.getTransient(ORIGINATING_ACTION_KEY), nullValue());
|
||||
}
|
||||
if (authorizationInfoHeader != null) {
|
||||
assertThat(threadContext.getTransient(AUTHORIZATION_INFO_KEY), sameInstance(authorizationInfoHeader));
|
||||
} else {
|
||||
assertThat(threadContext.getTransient(AUTHORIZATION_INFO_KEY), nullValue());
|
||||
}
|
||||
// but the authorization listener observes the authorization-resulting headers, which are different
|
||||
if (mockAccessControlHeader != null) {
|
||||
assertThat(indicesPermissions.actionGet(), not(sameInstance(mockAccessControlHeader)));
|
||||
}
|
||||
if (authorizationInfoHeader != null) {
|
||||
assertThat(authorizationInfo.actionGet(), not(sameInstance(authorizationInfoHeader)));
|
||||
}
|
||||
// except originating action, which is not overwritten
|
||||
if (originatingActionHeader != null) {
|
||||
assertThat(originatingAction.actionGet(), sameInstance(originatingActionHeader));
|
||||
}
|
||||
}
|
||||
|
||||
public void testActionsForSystemUserIsAuthorized() throws IOException {
|
||||
|
@ -14,6 +14,7 @@ import org.elasticsearch.xpack.core.security.SecurityContext;
|
||||
import org.elasticsearch.xpack.core.security.authc.Authentication;
|
||||
import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef;
|
||||
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
|
||||
import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField;
|
||||
import org.elasticsearch.xpack.core.security.user.AsyncSearchUser;
|
||||
import org.elasticsearch.xpack.core.security.user.SystemUser;
|
||||
import org.elasticsearch.xpack.core.security.user.User;
|
||||
@ -58,7 +59,7 @@ public class AuthorizationUtilsTests extends ESTestCase {
|
||||
User user = new User(randomAlphaOfLength(6), new String[] {});
|
||||
Authentication authentication = new Authentication(user, new RealmRef("test", "test", "foo"), null);
|
||||
threadContext.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication);
|
||||
threadContext.putTransient(AuthorizationService.ORIGINATING_ACTION_KEY, randomFrom("indices:foo", "cluster:bar"));
|
||||
threadContext.putTransient(AuthorizationServiceField.ORIGINATING_ACTION_KEY, randomFrom("indices:foo", "cluster:bar"));
|
||||
assertThat(AuthorizationUtils.shouldReplaceUserWithSystem(threadContext, "internal:something"), is(true));
|
||||
}
|
||||
|
||||
@ -66,7 +67,7 @@ public class AuthorizationUtilsTests extends ESTestCase {
|
||||
User user = new User(randomAlphaOfLength(6), new String[] {});
|
||||
Authentication authentication = new Authentication(user, new RealmRef("test", "test", "foo"), null);
|
||||
threadContext.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication);
|
||||
threadContext.putTransient(AuthorizationService.ORIGINATING_ACTION_KEY, randomFrom("internal:foo/bar"));
|
||||
threadContext.putTransient(AuthorizationServiceField.ORIGINATING_ACTION_KEY, randomFrom("internal:foo/bar"));
|
||||
assertThat(AuthorizationUtils.shouldReplaceUserWithSystem(threadContext, "internal:something"), is(false));
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,7 @@ import org.elasticsearch.transport.TransportService;
|
||||
import org.elasticsearch.xpack.core.security.SecurityContext;
|
||||
import org.elasticsearch.xpack.core.security.authc.Authentication;
|
||||
import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef;
|
||||
import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField;
|
||||
import org.elasticsearch.xpack.core.security.user.SystemUser;
|
||||
import org.elasticsearch.xpack.core.security.user.User;
|
||||
import org.elasticsearch.xpack.core.ssl.SSLService;
|
||||
@ -215,7 +216,7 @@ public class SecurityServerTransportInterceptorTests extends ESTestCase {
|
||||
final User user = new User("test", randomRoles(), authUser);
|
||||
final Authentication authentication = new Authentication(user, new RealmRef("ldap", "foo", "node1"), null);
|
||||
authentication.writeToContext(threadContext);
|
||||
threadContext.putTransient(AuthorizationService.ORIGINATING_ACTION_KEY, "indices:foo");
|
||||
threadContext.putTransient(AuthorizationServiceField.ORIGINATING_ACTION_KEY, "indices:foo");
|
||||
|
||||
SecurityServerTransportInterceptor interceptor = new SecurityServerTransportInterceptor(settings, threadPool,
|
||||
mock(AuthenticationService.class), mock(AuthorizationService.class), xPackLicenseState, mock(SSLService.class),
|
||||
@ -282,7 +283,7 @@ public class SecurityServerTransportInterceptorTests extends ESTestCase {
|
||||
final User user = new User("joe", randomRoles(), authUser);
|
||||
final Authentication authentication = new Authentication(user, new RealmRef("file", "file", "node1"), null);
|
||||
authentication.writeToContext(threadContext);
|
||||
threadContext.putTransient(AuthorizationService.ORIGINATING_ACTION_KEY, "indices:foo");
|
||||
threadContext.putTransient(AuthorizationServiceField.ORIGINATING_ACTION_KEY, "indices:foo");
|
||||
|
||||
SecurityServerTransportInterceptor interceptor = new SecurityServerTransportInterceptor(settings, threadPool,
|
||||
mock(AuthenticationService.class), mock(AuthorizationService.class), xPackLicenseState, mock(SSLService.class),
|
||||
@ -323,7 +324,7 @@ public class SecurityServerTransportInterceptorTests extends ESTestCase {
|
||||
final User user = new User("joe", randomRoles(), authUser);
|
||||
final Authentication authentication = new Authentication(user, new RealmRef("file", "file", "node1"), null);
|
||||
authentication.writeToContext(threadContext);
|
||||
threadContext.putTransient(AuthorizationService.ORIGINATING_ACTION_KEY, "indices:foo");
|
||||
threadContext.putTransient(AuthorizationServiceField.ORIGINATING_ACTION_KEY, "indices:foo");
|
||||
|
||||
SecurityServerTransportInterceptor interceptor = new SecurityServerTransportInterceptor(settings, threadPool,
|
||||
mock(AuthenticationService.class), mock(AuthorizationService.class), xPackLicenseState, mock(SSLService.class),
|
||||
|
Loading…
x
Reference in New Issue
Block a user