Skip to content

Commit 3fa765a

Browse files
authored
Resolve anonymous roles and deduplicate roles during authentication (elastic#53453)
Anonymous roles resolution and user role deduplication are now performed during authentication instead of authorization. The change ensures: * If anonymous access is enabled, user will be able to see the anonymous roles added in the roles field in the /_security/_authenticate response. * Any duplication in user roles are removed and will not show in the above authenticate response. * In any other case, the response is unchanged. It also introduces a behaviour change: the anonymous role resolution is now authentication node specific, previously it was authorization node specific. Details can be found at elastic#47195 (comment)
1 parent 316a2f1 commit 3fa765a

File tree

14 files changed

+125
-75
lines changed

14 files changed

+125
-75
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/AuthenticateResponse.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@ public void writeTo(StreamOutput out) throws IOException {
3434
authentication.writeTo(out);
3535
}
3636

37-
}
37+
}

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

+4
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ public boolean isRunAs() {
118118
return authenticatedUser != null;
119119
}
120120

121+
public User withRoles(String[] newRoles) {
122+
return new User(username, newRoles, fullName, email, metadata, enabled);
123+
}
124+
121125
@Override
122126
public String toString() {
123127
StringBuilder sb = new StringBuilder();

x-pack/plugin/security/qa/basic-enable-security/src/test/java/org/elasticsearch/xpack/security/EnableSecurityOnBasicLicenseIT.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import java.util.Map;
2525

2626
import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
27-
import static org.hamcrest.Matchers.contains;
27+
import static org.hamcrest.Matchers.containsInAnyOrder;
2828
import static org.hamcrest.Matchers.containsString;
2929
import static org.hamcrest.Matchers.equalTo;
3030
import static org.hamcrest.Matchers.notNullValue;
@@ -126,7 +126,7 @@ private void checkAuthentication() throws IOException {
126126
final Map<String, Object> auth = getAsMap("/_security/_authenticate");
127127
// From file realm, configured in build.gradle
128128
assertThat(ObjectPath.evaluate(auth, "username"), equalTo("security_test_user"));
129-
assertThat(ObjectPath.evaluate(auth, "roles"), contains("security_test_role"));
129+
assertThat(ObjectPath.evaluate(auth, "roles"), containsInAnyOrder("security_test_role", "anonymous"));
130130
}
131131

132132
private void checkAllowedWrite(String indexName) throws IOException {

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

+32-1
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,14 @@
4949
import org.elasticsearch.xpack.security.support.SecurityIndexManager;
5050

5151
import java.util.ArrayList;
52+
import java.util.Arrays;
5253
import java.util.Collections;
5354
import java.util.LinkedHashMap;
55+
import java.util.LinkedHashSet;
5456
import java.util.List;
5557
import java.util.Map;
5658
import java.util.Objects;
59+
import java.util.Set;
5760
import java.util.concurrent.atomic.AtomicLong;
5861
import java.util.function.BiConsumer;
5962
import java.util.function.Consumer;
@@ -656,7 +659,8 @@ void finishAuthentication(User finalUser) {
656659
logger.debug("user [{}] is disabled. failing authentication", finalUser);
657660
listener.onFailure(request.authenticationFailed(authenticationToken));
658661
} else {
659-
final Authentication finalAuth = new Authentication(finalUser, authenticatedBy, lookedupBy);
662+
final Authentication finalAuth = new Authentication(
663+
maybeConsolidateRolesForUser(finalUser), authenticatedBy, lookedupBy);
660664
writeAuthToContext(finalAuth);
661665
}
662666
}
@@ -689,6 +693,33 @@ void writeAuthToContext(Authentication authentication) {
689693
private void authenticateToken(AuthenticationToken token) {
690694
this.consumeToken(token);
691695
}
696+
697+
private User maybeConsolidateRolesForUser(User user) {
698+
if (User.isInternal(user)) {
699+
return user;
700+
} else if (isAnonymousUserEnabled && anonymousUser.equals(user) == false) {
701+
if (anonymousUser.roles().length == 0) {
702+
throw new IllegalStateException("anonymous is only enabled when the anonymous user has roles");
703+
}
704+
User userWithMergedRoles = user.withRoles(mergeRoles(user.roles(), anonymousUser.roles()));
705+
if (user.isRunAs()) {
706+
final User authUserWithMergedRoles = user.authenticatedUser().withRoles(
707+
mergeRoles(user.authenticatedUser().roles(), anonymousUser.roles()));
708+
userWithMergedRoles = new User(userWithMergedRoles, authUserWithMergedRoles);
709+
}
710+
return userWithMergedRoles;
711+
} else {
712+
return user;
713+
}
714+
}
715+
716+
private String[] mergeRoles(String[] existingRoles, String[] otherRoles) {
717+
Set<String> roles = new LinkedHashSet<>(Arrays.asList(existingRoles));
718+
if (otherRoles != null) {
719+
Collections.addAll(roles, otherRoles);
720+
}
721+
return roles.toArray(new String[0]);
722+
}
692723
}
693724

694725
abstract static class AuditableRequest {

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,21 @@
2424
*/
2525
public class NativeRealm extends CachingUsernamePasswordRealm {
2626

27-
private final NativeUsersStore userStore;
27+
private final NativeUsersStore usersStore;
2828

2929
public NativeRealm(RealmConfig config, NativeUsersStore usersStore, ThreadPool threadPool) {
3030
super(config, threadPool);
31-
this.userStore = usersStore;
31+
this.usersStore = usersStore;
3232
}
3333

3434
@Override
3535
protected void doLookupUser(String username, ActionListener<User> listener) {
36-
userStore.getUser(username, listener);
36+
usersStore.getUser(username, listener);
3737
}
3838

3939
@Override
4040
protected void doAuthenticate(UsernamePasswordToken token, ActionListener<AuthenticationResult> listener) {
41-
userStore.verifyPassword(token.principal(), token.credentials(), listener);
41+
usersStore.verifyPassword(token.principal(), token.credentials(), listener);
4242
}
4343

4444
public void onSecurityIndexStateChange(SecurityIndexManager.State previousState, SecurityIndexManager.State currentState) {
@@ -50,7 +50,7 @@ public void onSecurityIndexStateChange(SecurityIndexManager.State previousState,
5050
@Override
5151
public void usageStats(ActionListener<Map<String, Object>> listener) {
5252
super.usageStats(ActionListener.wrap(stats ->
53-
userStore.getUserCount(ActionListener.wrap(size -> {
53+
usersStore.getUserCount(ActionListener.wrap(size -> {
5454
stats.put("size", size);
5555
listener.onResponse(stats);
5656
}, listener::onFailure))

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import java.util.Collection;
5858
import java.util.Collections;
5959
import java.util.HashMap;
60+
import java.util.LinkedHashSet;
6061
import java.util.List;
6162
import java.util.Map;
6263
import java.util.function.Consumer;
@@ -643,7 +644,7 @@ private UserAndPassword transformUser(final String id, final Map<String, Object>
643644
final String username = id.substring(USER_DOC_TYPE.length() + 1);
644645
try {
645646
String password = (String) sourceMap.get(Fields.PASSWORD.getPreferredName());
646-
String[] roles = ((List<String>) sourceMap.get(Fields.ROLES.getPreferredName())).toArray(Strings.EMPTY_ARRAY);
647+
String[] roles = new LinkedHashSet<>((List<String>)sourceMap.get(Fields.ROLES.getPreferredName())).toArray(Strings.EMPTY_ARRAY);
647648
String fullName = (String) sourceMap.get(Fields.FULL_NAME.getPreferredName());
648649
String email = (String) sourceMap.get(Fields.EMAIL.getPreferredName());
649650
Boolean enabled = (Boolean) sourceMap.get(Fields.ENABLED.getPreferredName());

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.util.ArrayList;
3131
import java.util.Collections;
3232
import java.util.HashMap;
33+
import java.util.LinkedHashSet;
3334
import java.util.List;
3435
import java.util.Locale;
3536
import java.util.Map;
@@ -164,7 +165,7 @@ public static Map<String, String[]> parseFile(Path path, @Nullable Logger logger
164165

165166
Map<String, String[]> usersRoles = new HashMap<>();
166167
for (Map.Entry<String, List<String>> entry : userToRoles.entrySet()) {
167-
usersRoles.put(entry.getKey(), entry.getValue().toArray(new String[entry.getValue().size()]));
168+
usersRoles.put(entry.getKey(), new LinkedHashSet<>(entry.getValue()).toArray(new String[0]));
168169
}
169170

170171
logger.debug("parsed [{}] user to role mappings from file [{}]", usersRoles.size(), path.toAbsolutePath());

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

+2-9
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,8 @@
6060
import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver;
6161
import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege;
6262
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
63-
import org.elasticsearch.xpack.core.security.user.AsyncSearchUser;
6463
import org.elasticsearch.xpack.core.security.user.SystemUser;
6564
import org.elasticsearch.xpack.core.security.user.User;
66-
import org.elasticsearch.xpack.core.security.user.XPackSecurityUser;
67-
import org.elasticsearch.xpack.core.security.user.XPackUser;
6865
import org.elasticsearch.xpack.security.audit.AuditLevel;
6966
import org.elasticsearch.xpack.security.audit.AuditTrail;
7067
import org.elasticsearch.xpack.security.audit.AuditTrailService;
@@ -173,7 +170,7 @@ public void authorize(final Authentication authentication, final String action,
173170
if (auditId == null) {
174171
// We would like to assert that there is an existing request-id, but if this is a system action, then that might not be
175172
// true because the request-id is generated during authentication
176-
if (isInternalUser(authentication.getUser()) != false) {
173+
if (User.isInternal(authentication.getUser()) != false) {
177174
auditId = AuditUtil.getOrGenerateRequestId(threadContext);
178175
} else {
179176
auditTrailService.get().tamperedRequest(null, authentication.getUser(), action, originalRequest);
@@ -368,7 +365,7 @@ AuthorizationEngine getAuthorizationEngine(final Authentication authentication)
368365
private AuthorizationEngine getAuthorizationEngineForUser(final User user) {
369366
if (rbacEngine != authorizationEngine && licenseState.isSecurityEnabled() &&
370367
licenseState.isAllowed(Feature.SECURITY_AUTHORIZATION_ENGINE)) {
371-
if (ClientReservedRealm.isReserved(user.principal(), settings) || isInternalUser(user)) {
368+
if (ClientReservedRealm.isReserved(user.principal(), settings) || User.isInternal(user)) {
372369
return rbacEngine;
373370
} else {
374371
return authorizationEngine;
@@ -419,10 +416,6 @@ private TransportRequest maybeUnwrapRequest(Authentication authentication, Trans
419416
return request;
420417
}
421418

422-
private boolean isInternalUser(User user) {
423-
return SystemUser.is(user) || XPackUser.is(user) || XPackSecurityUser.is(user) || AsyncSearchUser.is(user);
424-
}
425-
426419
private void authorizeRunAs(final RequestInfo requestInfo, final AuthorizationInfo authzInfo,
427420
final ActionListener<AuthorizationResult> listener) {
428421
final Authentication authentication = requestInfo.getAuthentication();

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

-12
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult;
4545
import org.elasticsearch.xpack.core.security.support.CacheIteratorHelper;
4646
import org.elasticsearch.xpack.core.security.support.MetadataUtils;
47-
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
4847
import org.elasticsearch.xpack.core.security.user.AsyncSearchUser;
4948
import org.elasticsearch.xpack.core.security.user.SystemUser;
5049
import org.elasticsearch.xpack.core.security.user.User;
@@ -101,9 +100,7 @@ public class CompositeRolesStore {
101100
private final DocumentSubsetBitsetCache dlsBitsetCache;
102101
private final ThreadContext threadContext;
103102
private final AtomicLong numInvalidation = new AtomicLong();
104-
private final AnonymousUser anonymousUser;
105103
private final ApiKeyService apiKeyService;
106-
private final boolean isAnonymousEnabled;
107104
private final List<BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>>> builtInRoleProviders;
108105
private final List<BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>>> allRoleProviders;
109106

@@ -146,8 +143,6 @@ public CompositeRolesStore(Settings settings, FileRolesStore fileRolesStore, Nat
146143
allList.addAll(rolesProviders);
147144
this.allRoleProviders = Collections.unmodifiableList(allList);
148145
}
149-
this.anonymousUser = new AnonymousUser(settings);
150-
this.isAnonymousEnabled = AnonymousUser.isAnonymousEnabled(settings);
151146
}
152147

153148
public void roles(Set<String> roleNames, ActionListener<Role> roleActionListener) {
@@ -237,13 +232,6 @@ public void getRoles(User user, Authentication authentication, ActionListener<Ro
237232
}, roleActionListener::onFailure));
238233
} else {
239234
Set<String> roleNames = new HashSet<>(Arrays.asList(user.roles()));
240-
if (isAnonymousEnabled && anonymousUser.equals(user) == false) {
241-
if (anonymousUser.roles().length == 0) {
242-
throw new IllegalStateException("anonymous is only enabled when the anonymous user has roles");
243-
}
244-
Collections.addAll(roleNames, anonymousUser.roles());
245-
}
246-
247235
if (roleNames.isEmpty()) {
248236
roleActionListener.onResponse(Role.EMPTY);
249237
} else if (roleNames.contains(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName())) {

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

+46
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,11 @@
7474
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.EmptyAuthorizationInfo;
7575
import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames;
7676
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
77+
import org.elasticsearch.xpack.core.security.user.AsyncSearchUser;
7778
import org.elasticsearch.xpack.core.security.user.SystemUser;
7879
import org.elasticsearch.xpack.core.security.user.User;
80+
import org.elasticsearch.xpack.core.security.user.XPackSecurityUser;
81+
import org.elasticsearch.xpack.core.security.user.XPackUser;
7982
import org.elasticsearch.xpack.security.audit.AuditTrail;
8083
import org.elasticsearch.xpack.security.audit.AuditTrailService;
8184
import org.elasticsearch.xpack.security.audit.AuditUtil;
@@ -108,6 +111,7 @@
108111
import static org.elasticsearch.xpack.core.security.support.Exceptions.authenticationError;
109112
import static org.elasticsearch.xpack.security.authc.TokenServiceTests.mockGetTokenFromId;
110113
import static org.hamcrest.Matchers.arrayContaining;
114+
import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
111115
import static org.hamcrest.Matchers.contains;
112116
import static org.hamcrest.Matchers.containsString;
113117
import static org.hamcrest.Matchers.emptyOrNullString;
@@ -902,6 +906,48 @@ public void testAnonymousUserTransportWithDefaultUser() throws Exception {
902906
assertThreadContextContainsAuthentication(result);
903907
}
904908

909+
public void testInheritAnonymousUserRoles() {
910+
Settings settings = Settings.builder()
911+
.putList(AnonymousUser.ROLES_SETTING.getKey(), "r3", "r4", "r5")
912+
.build();
913+
final AnonymousUser anonymousUser = new AnonymousUser(settings);
914+
service = new AuthenticationService(settings, realms, auditTrailService,
915+
new DefaultAuthenticationFailureHandler(Collections.emptyMap()),
916+
threadPool, anonymousUser, tokenService, apiKeyService);
917+
User user1 = new User("username", "r1", "r2", "r3");
918+
when(firstRealm.token(threadContext)).thenReturn(token);
919+
when(firstRealm.supports(token)).thenReturn(true);
920+
mockAuthenticate(firstRealm, token, user1);
921+
// this call does not actually go async
922+
final AtomicBoolean completed = new AtomicBoolean(false);
923+
service.authenticate(restRequest, true, ActionListener.wrap(authentication -> {
924+
assertThat(authentication.getUser().roles(), arrayContainingInAnyOrder("r1", "r2", "r3", "r4", "r5"));
925+
setCompletedToTrue(completed);
926+
}, this::logAndFail));
927+
assertTrue(completed.get());
928+
}
929+
930+
public void testSystemUsersDoNotInheritAnonymousRoles() {
931+
Settings settings = Settings.builder()
932+
.putList(AnonymousUser.ROLES_SETTING.getKey(), "r3", "r4", "r5")
933+
.build();
934+
final AnonymousUser anonymousUser = new AnonymousUser(settings);
935+
service = new AuthenticationService(settings, realms, auditTrailService,
936+
new DefaultAuthenticationFailureHandler(Collections.emptyMap()),
937+
threadPool, anonymousUser, tokenService, apiKeyService);
938+
when(firstRealm.token(threadContext)).thenReturn(token);
939+
when(firstRealm.supports(token)).thenReturn(true);
940+
final User sysUser = randomFrom(SystemUser.INSTANCE, XPackUser.INSTANCE, XPackSecurityUser.INSTANCE, AsyncSearchUser.INSTANCE);
941+
mockAuthenticate(firstRealm, token, sysUser);
942+
// this call does not actually go async
943+
final AtomicBoolean completed = new AtomicBoolean(false);
944+
service.authenticate(restRequest, true, ActionListener.wrap(authentication -> {
945+
assertThat(authentication.getUser().roles(), equalTo(sysUser.roles()));
946+
setCompletedToTrue(completed);
947+
}, this::logAndFail));
948+
assertTrue(completed.get());
949+
}
950+
905951
public void testRealmTokenThrowingException() throws Exception {
906952
final String reqId = AuditUtil.getOrGenerateRequestId(threadContext);
907953
when(firstRealm.token(threadContext)).thenThrow(authenticationError("realm doesn't like tokens"));

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java

+16
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import static org.hamcrest.CoreMatchers.equalTo;
5353
import static org.hamcrest.CoreMatchers.notNullValue;
5454
import static org.hamcrest.CoreMatchers.nullValue;
55+
import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
5556
import static org.mockito.Matchers.any;
5657
import static org.mockito.Mockito.doAnswer;
5758
import static org.mockito.Mockito.mock;
@@ -154,6 +155,21 @@ public void testVerifyUserWithCorrectPassword() throws Exception {
154155
}
155156

156157
public void testVerifyUserWithIncorrectPassword() throws Exception {
158+
final NativeUsersStore nativeUsersStore = startNativeUsersStore();
159+
final String username = randomAlphaOfLengthBetween(4, 12);
160+
final SecureString password = new SecureString(randomAlphaOfLengthBetween(12, 16).toCharArray());
161+
final List<String> roles = randomList(1, 4, () -> randomAlphaOfLength(12));
162+
roles.add(randomIntBetween(0, roles.size()), roles.get(0));
163+
164+
final PlainActionFuture<AuthenticationResult> future = new PlainActionFuture<>();
165+
nativeUsersStore.verifyPassword(username, password, future);
166+
respondToGetUserRequest(username, password, roles.toArray(new String[0]));
167+
168+
final AuthenticationResult result = future.get();
169+
assertThat(result.getUser().roles(), arrayContainingInAnyOrder(roles.stream().distinct().toArray()));
170+
}
171+
172+
public void testDeduplicateUserRoles() throws Exception {
157173
final NativeUsersStore nativeUsersStore = startNativeUsersStore();
158174
final String username = randomAlphaOfLengthBetween(4, 12);
159175
final SecureString correctPassword = new SecureString(randomAlphaOfLengthBetween(12, 16).toCharArray());

0 commit comments

Comments
 (0)