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:
Albert Zaharovits 2020-09-15 16:37:38 +03:00 committed by GitHub
parent 76f56c1264
commit aeed1c05b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 308 additions and 101 deletions

View File

@ -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);
}

View File

@ -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);

View File

@ -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() {}
}

View File

@ -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;

View File

@ -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 {

View File

@ -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;
}

View File

@ -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.

View File

@ -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));
}
}

View File

@ -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 {

View File

@ -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));
}

View File

@ -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),