Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disabling users includes realm in same-user validation #86473

Merged
merged 14 commits into from
May 6, 2022
5 changes: 5 additions & 0 deletions docs/changelog/86473.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 86473
summary: Setting enabled user status accounts for source realm in same user check
area: Authentication
type: bug
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
import org.elasticsearch.xpack.core.security.SecurityContext;
import org.elasticsearch.xpack.core.security.action.user.SetEnabledAction;
import org.elasticsearch.xpack.core.security.action.user.SetEnabledRequest;
import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore;
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;

/**
* Transport action that handles setting a native or reserved user to enabled
Expand Down Expand Up @@ -47,7 +49,7 @@ public TransportSetEnabledAction(
protected void doExecute(Task task, SetEnabledRequest request, ActionListener<ActionResponse.Empty> listener) {
final String username = request.username();
// make sure the user is not disabling themselves
if (securityContext.getUser().principal().equals(request.username())) {
if (isSameUserRequest(request)) {
listener.onFailure(new IllegalArgumentException("users may not update the enabled status of their own account"));
return;
} else if (AnonymousUser.isAnonymousUsername(username, settings)) {
Expand All @@ -62,4 +64,16 @@ protected void doExecute(Task task, SetEnabledRequest request, ActionListener<Ac
listener.delegateFailure((l, v) -> l.onResponse(ActionResponse.Empty.INSTANCE))
);
}

private boolean isSameUserRequest(SetEnabledRequest request) {
final var effectiveSubject = securityContext.getAuthentication().getEffectiveSubject();
final var realmType = effectiveSubject.getRealm().getType();
if (ReservedRealm.TYPE.equals(realmType) == false && NativeRealmSettings.TYPE.equals(realmType) == false) {
// Only native or reserved realm users can be disabled via the API. If the realm of the effective subject is neither,
// the target must be a different user
return false;
} else {
return effectiveSubject.getUser().principal().equals(request.username());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@
import org.elasticsearch.xpack.core.security.action.user.SetEnabledRequest;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper;
import org.elasticsearch.xpack.core.security.authc.Subject;
import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer;
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
import org.elasticsearch.xpack.core.security.user.ElasticUser;
import org.elasticsearch.xpack.core.security.user.KibanaUser;
import org.elasticsearch.xpack.core.security.user.SystemUser;
import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore;
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;

import java.io.IOException;
import java.util.Collections;
Expand All @@ -42,6 +45,7 @@
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
Expand All @@ -60,8 +64,9 @@ public void testAnonymousUser() throws Exception {
ThreadPool threadPool = mock(ThreadPool.class);
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
when(threadPool.getThreadContext()).thenReturn(threadContext);
Authentication authentication = mock(Authentication.class);
when(authentication.getUser()).thenReturn(user);
Authentication authentication = mock(Authentication.class, RETURNS_DEEP_STUBS);
when(authentication.getEffectiveSubject().getUser()).thenReturn(user);
when(authentication.getEffectiveSubject().getRealm().getType()).thenReturn(NativeRealmSettings.TYPE);
when(authentication.encode()).thenReturn(randomAlphaOfLength(24)); // just can't be null
new AuthenticationContextSerializer().writeToContext(authentication, threadContext);

Expand Down Expand Up @@ -121,8 +126,9 @@ private void testValidUser(User user) throws IOException {
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
when(threadPool.getThreadContext()).thenReturn(threadContext);

Authentication authentication = mock(Authentication.class);
when(authentication.getUser()).thenReturn(new User("the runner"));
Authentication authentication = mock(Authentication.class, RETURNS_DEEP_STUBS);
when(authentication.getEffectiveSubject().getUser()).thenReturn(new User("the runner"));
when(authentication.getEffectiveSubject().getRealm().getType()).thenReturn(NativeRealmSettings.TYPE);
when(authentication.encode()).thenReturn(randomAlphaOfLength(24)); // just can't be null
new AuthenticationContextSerializer().writeToContext(authentication, threadContext);

Expand Down Expand Up @@ -188,8 +194,9 @@ public void testException() throws Exception {
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
when(threadPool.getThreadContext()).thenReturn(threadContext);

Authentication authentication = mock(Authentication.class);
when(authentication.getUser()).thenReturn(new User("the runner"));
Authentication authentication = mock(Authentication.class, RETURNS_DEEP_STUBS);
when(authentication.getEffectiveSubject().getUser()).thenReturn(new User("the runner"));
when(authentication.getEffectiveSubject().getRealm().getType()).thenReturn(NativeRealmSettings.TYPE);
when(authentication.encode()).thenReturn(randomAlphaOfLength(24)); // just can't be null
new AuthenticationContextSerializer().writeToContext(authentication, threadContext);

Expand Down Expand Up @@ -252,14 +259,87 @@ public void onFailure(Exception e) {
);
}

public void testUserCanModifySameNameUserFromDifferentRealm() throws Exception {
final User user = randomFrom(new ElasticUser(true), new KibanaUser(true), new User("joe"));

ThreadPool threadPool = mock(ThreadPool.class);
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
when(threadPool.getThreadContext()).thenReturn(threadContext);

Authentication authentication = mock(Authentication.class, RETURNS_DEEP_STUBS);
when(authentication.getEffectiveSubject().getUser()).thenReturn(user);
when(authentication.getEffectiveSubject().getRealm().getType()).thenReturn("other_realm");
when(authentication.encode()).thenReturn(randomAlphaOfLength(24)); // just can't be null
new AuthenticationContextSerializer().writeToContext(authentication, threadContext);

NativeUsersStore usersStore = mock(NativeUsersStore.class);
SetEnabledRequest request = new SetEnabledRequest();
request.username(user.principal());
request.enabled(randomBoolean());
request.setRefreshPolicy(randomFrom(RefreshPolicy.values()));
// mock the setEnabled call on the native users store so that it will invoke the action listener with a response
doAnswer(invocation -> {
Object[] args = invocation.getArguments();
assert args.length == 4;
@SuppressWarnings("unchecked")
ActionListener<Void> listener = (ActionListener<Void>) args[3];
listener.onResponse(null);
return null;
}).when(usersStore).setEnabled(eq(user.principal()), eq(request.enabled()), eq(request.getRefreshPolicy()), anyActionListener());
TransportService transportService = new TransportService(
Settings.EMPTY,
mock(Transport.class),
threadPool,
TransportService.NOOP_TRANSPORT_INTERCEPTOR,
x -> null,
null,
Collections.emptySet()
);
final SecurityContext securityContext = new SecurityContext(Settings.EMPTY, threadContext);
TransportSetEnabledAction action = new TransportSetEnabledAction(
Settings.EMPTY,
transportService,
mock(ActionFilters.class),
securityContext,
usersStore
);

final AtomicReference<Throwable> throwableRef = new AtomicReference<>();
final AtomicReference<ActionResponse.Empty> responseRef = new AtomicReference<>();
action.doExecute(mock(Task.class), request, new ActionListener<>() {
@Override
public void onResponse(ActionResponse.Empty setEnabledResponse) {
responseRef.set(setEnabledResponse);
}

@Override
public void onFailure(Exception e) {
throwableRef.set(e);
}
});

assertThat(responseRef.get(), is(notNullValue()));
assertSame(responseRef.get(), ActionResponse.Empty.INSTANCE);
assertThat(throwableRef.get(), is(nullValue()));
verify(usersStore, times(1)).setEnabled(
eq(user.principal()),
eq(request.enabled()),
eq(request.getRefreshPolicy()),
anyActionListener()
);
}

public void testUserModifyingThemselves() throws Exception {
final User user = randomFrom(new ElasticUser(true), new KibanaUser(true), new User("joe"));
ThreadPool threadPool = mock(ThreadPool.class);
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
when(threadPool.getThreadContext()).thenReturn(threadContext);

Authentication authentication = mock(Authentication.class);
when(authentication.getUser()).thenReturn(user);
Subject effectiveSubject = mock(Subject.class, RETURNS_DEEP_STUBS);
when(authentication.getEffectiveSubject()).thenReturn(effectiveSubject);
when(effectiveSubject.getUser()).thenReturn(user);
when(effectiveSubject.getRealm().getType()).thenReturn(randomFrom(NativeRealmSettings.TYPE, ReservedRealm.TYPE));
when(authentication.encode()).thenReturn(randomAlphaOfLength(24)); // just can't be null
new AuthenticationContextSerializer().writeToContext(authentication, threadContext);

Expand Down