Skip to content

Commit d1548ab

Browse files
committed
Use consistent view of realms for authentication (#38815)
This change updates the authentication service to use a consistent view of the realms based on the license state at the start of authentication. Without this, the license can change during authentication of a request and it will result in a failure if the realm that extracted the token is no longer in the realm list. This manifests in some tests as an authentication failure that should never really happen; one example would be the test framework's transport client user should always have a succesful authentication but in the LicensingTests this can fail and will show up as a NoNodeAvailableException. Additionally, the licensing tests have been updated to ensure that there is consistency when changing the license. The license is changed by modifying the internal xpack license state on each node, which has no protection against be changed by some pending cluster action. The methods to disable and enable now ensure we have a green cluster and that the cluster is consistent before returning. Closes #30301
1 parent a66bbd6 commit d1548ab

File tree

9 files changed

+135
-64
lines changed

9 files changed

+135
-64
lines changed

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,15 @@ it to the action without an associated user (not via REST or transport - this is
154154
*/
155155
final String securityAction = actionMapper.action(action, request);
156156
authcService.authenticate(securityAction, request, SystemUser.INSTANCE,
157-
ActionListener.wrap((authc) -> authorizeRequest(authc, securityAction, request, listener), listener::onFailure));
157+
ActionListener.wrap((authc) -> {
158+
if (authc != null) {
159+
authorizeRequest(authc, securityAction, request, listener);
160+
} else if (licenseState.isAuthAllowed() == false) {
161+
listener.onResponse(null);
162+
} else {
163+
listener.onFailure(new IllegalStateException("no authentication present but auth is allowed"));
164+
}
165+
}, listener::onFailure));
158166
}
159167

160168
private <Request extends ActionRequest> void authorizeRequest(Authentication authentication, String securityAction, Request request,

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlAuthenticateAction.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ protected void doExecute(SamlAuthenticateRequest request,
6060
listener.onFailure(new IllegalStateException("Cannot find AuthenticationResult on thread context"));
6161
return;
6262
}
63+
assert authentication != null : "authentication should never be null at this point";
6364
final Map<String, Object> tokenMeta = (Map<String, Object>) result.getMetadata().get(SamlRealm.CONTEXT_TOKEN_DATA);
6465
tokenService.createUserToken(authentication, originatingAuthentication,
6566
ActionListener.wrap(tuple -> {

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenAction.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@ private void authenticateAndCreateToken(CreateTokenRequest request, ActionListen
7373
authenticationService.authenticate(CreateTokenAction.NAME, request, authToken,
7474
ActionListener.wrap(authentication -> {
7575
request.getPassword().close();
76-
createToken(request, authentication, originatingAuthentication, true, listener);
76+
if (authentication != null) {
77+
createToken(request, authentication, originatingAuthentication, true, listener);
78+
} else {
79+
listener.onFailure(new UnsupportedOperationException("cannot create token if authentication is not allowed"));
80+
}
7781
}, e -> {
7882
// clear the request password
7983
request.getPassword().close();

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

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,9 @@ class Authenticator {
135135

136136
private final AuditableRequest request;
137137
private final User fallbackUser;
138-
138+
private final List<Realm> defaultOrderedRealmList;
139139
private final ActionListener<Authentication> listener;
140+
140141
private RealmRef authenticatedBy = null;
141142
private RealmRef lookedupBy = null;
142143
private AuthenticationToken authenticationToken = null;
@@ -154,6 +155,7 @@ class Authenticator {
154155
private Authenticator(AuditableRequest auditableRequest, User fallbackUser, ActionListener<Authentication> listener) {
155156
this.request = auditableRequest;
156157
this.fallbackUser = fallbackUser;
158+
this.defaultOrderedRealmList = realms.asList();
157159
this.listener = listener;
158160
}
159161

@@ -172,27 +174,33 @@ private Authenticator(AuditableRequest auditableRequest, User fallbackUser, Acti
172174
* </ol>
173175
*/
174176
private void authenticateAsync() {
175-
lookForExistingAuthentication((authentication) -> {
176-
if (authentication != null) {
177-
listener.onResponse(authentication);
178-
} else {
179-
tokenService.getAndValidateToken(threadContext, ActionListener.wrap(userToken -> {
180-
if (userToken != null) {
181-
writeAuthToContext(userToken.getAuthentication());
182-
} else {
183-
extractToken(this::consumeToken);
184-
}
185-
}, e -> {
186-
if (e instanceof ElasticsearchSecurityException &&
177+
if (defaultOrderedRealmList.isEmpty()) {
178+
// this happens when the license state changes between the call to authenticate and the actual invocation
179+
// to get the realm list
180+
listener.onResponse(null);
181+
} else {
182+
lookForExistingAuthentication((authentication) -> {
183+
if (authentication != null) {
184+
listener.onResponse(authentication);
185+
} else {
186+
tokenService.getAndValidateToken(threadContext, ActionListener.wrap(userToken -> {
187+
if (userToken != null) {
188+
writeAuthToContext(userToken.getAuthentication());
189+
} else {
190+
extractToken(this::consumeToken);
191+
}
192+
}, e -> {
193+
if (e instanceof ElasticsearchSecurityException &&
187194
tokenService.isExpiredTokenException((ElasticsearchSecurityException) e) == false) {
188-
// intentionally ignore the returned exception; we call this primarily
189-
// for the auditing as we already have a purpose built exception
190-
request.tamperedRequest();
191-
}
192-
listener.onFailure(e);
193-
}));
194-
}
195-
});
195+
// intentionally ignore the returned exception; we call this primarily
196+
// for the auditing as we already have a purpose built exception
197+
request.tamperedRequest();
198+
}
199+
listener.onFailure(e);
200+
}));
201+
}
202+
});
203+
}
196204
}
197205

198206
/**
@@ -233,7 +241,7 @@ void extractToken(Consumer<AuthenticationToken> consumer) {
233241
if (authenticationToken != null) {
234242
action = () -> consumer.accept(authenticationToken);
235243
} else {
236-
for (Realm realm : realms) {
244+
for (Realm realm : defaultOrderedRealmList) {
237245
final AuthenticationToken token = realm.token(threadContext);
238246
if (token != null) {
239247
action = () -> consumer.accept(token);
@@ -260,7 +268,6 @@ private void consumeToken(AuthenticationToken token) {
260268
handleNullToken();
261269
} else {
262270
authenticationToken = token;
263-
final List<Realm> realmsList = realms.asList();
264271
final Map<Realm, Tuple<String, Exception>> messages = new LinkedHashMap<>();
265272
final BiConsumer<Realm, ActionListener<User>> realmAuthenticatingConsumer = (realm, userListener) -> {
266273
if (realm.supports(authenticationToken)) {
@@ -297,11 +304,12 @@ private void consumeToken(AuthenticationToken token) {
297304
userListener.onResponse(null);
298305
}
299306
};
307+
300308
final IteratingActionListener<User, Realm> authenticatingListener =
301309
new IteratingActionListener<>(ContextPreservingActionListener.wrapPreservingContext(ActionListener.wrap(
302310
(user) -> consumeUser(user, messages),
303311
(e) -> listener.onFailure(request.exceptionProcessingRequest(e, token))), threadContext),
304-
realmAuthenticatingConsumer, realmsList, threadContext);
312+
realmAuthenticatingConsumer, defaultOrderedRealmList, threadContext);
305313
try {
306314
authenticatingListener.run();
307315
} catch (Exception e) {
@@ -388,7 +396,7 @@ private void consumeUser(User user, Map<Realm, Tuple<String, Exception>> message
388396
* names of users that exist using a timing attack
389397
*/
390398
private void lookupRunAsUser(final User user, String runAsUsername, Consumer<User> userConsumer) {
391-
final RealmUserLookup lookup = new RealmUserLookup(realms.asList(), threadContext);
399+
final RealmUserLookup lookup = new RealmUserLookup(defaultOrderedRealmList, threadContext);
392400
lookup.lookup(runAsUsername, ActionListener.wrap(tuple -> {
393401
if (tuple == null) {
394402
// the user does not exist, but we still create a User object, which will later be rejected by authz

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,12 @@ protected Map<String, ServerTransportFilter> initializeProfileFilters(Destructiv
187187
case "client":
188188
profileFilters.put(entry.getKey(), new ServerTransportFilter.ClientProfile(authcService, authzService,
189189
threadPool.getThreadContext(), extractClientCert, destructiveOperations, reservedRealmEnabled,
190-
securityContext));
190+
securityContext, licenseState));
191191
break;
192192
case "node":
193193
profileFilters.put(entry.getKey(), new ServerTransportFilter.NodeProfile(authcService, authzService,
194194
threadPool.getThreadContext(), extractClientCert, destructiveOperations, reservedRealmEnabled,
195-
securityContext));
195+
securityContext, licenseState));
196196
break;
197197
default:
198198
throw new IllegalStateException("unknown profile type: " + type);

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.elasticsearch.action.admin.indices.open.OpenIndexAction;
2020
import org.elasticsearch.action.support.DestructiveOperations;
2121
import org.elasticsearch.common.util.concurrent.ThreadContext;
22+
import org.elasticsearch.license.XPackLicenseState;
2223
import org.elasticsearch.transport.TaskTransportChannel;
2324
import org.elasticsearch.transport.TcpTransportChannel;
2425
import org.elasticsearch.transport.TransportChannel;
@@ -78,17 +79,19 @@ class NodeProfile implements ServerTransportFilter {
7879
private final DestructiveOperations destructiveOperations;
7980
private final boolean reservedRealmEnabled;
8081
private final SecurityContext securityContext;
82+
private final XPackLicenseState licenseState;
8183

8284
NodeProfile(AuthenticationService authcService, AuthorizationService authzService,
8385
ThreadContext threadContext, boolean extractClientCert, DestructiveOperations destructiveOperations,
84-
boolean reservedRealmEnabled, SecurityContext securityContext) {
86+
boolean reservedRealmEnabled, SecurityContext securityContext, XPackLicenseState licenseState) {
8587
this.authcService = authcService;
8688
this.authzService = authzService;
8789
this.threadContext = threadContext;
8890
this.extractClientCert = extractClientCert;
8991
this.destructiveOperations = destructiveOperations;
9092
this.reservedRealmEnabled = reservedRealmEnabled;
9193
this.securityContext = securityContext;
94+
this.licenseState = licenseState;
9295
}
9396

9497
@Override
@@ -129,6 +132,7 @@ requests from all the nodes are attached with a user (either a serialize
129132

130133
final Version version = transportChannel.getVersion().equals(Version.V_5_4_0) ? Version.CURRENT : transportChannel.getVersion();
131134
authcService.authenticate(securityAction, request, (User)null, ActionListener.wrap((authentication) -> {
135+
if (authentication != null) {
132136
if (reservedRealmEnabled && authentication.getVersion().before(Version.V_5_2_0) &&
133137
KibanaUser.NAME.equals(authentication.getUser().authenticatedUser().principal())) {
134138
executeAsCurrentVersionKibanaUser(securityAction, request, transportChannel, listener, authentication);
@@ -156,7 +160,12 @@ requests from all the nodes are attached with a user (either a serialize
156160
});
157161
asyncAuthorizer.authorize(authzService);
158162
}
159-
}, listener::onFailure));
163+
} else if (licenseState.isAuthAllowed() == false) {
164+
listener.onResponse(null);
165+
} else {
166+
listener.onFailure(new IllegalStateException("no authentication present but auth is allowed"));
167+
}
168+
}, listener::onFailure));
160169
}
161170

162171
private void executeAsCurrentVersionKibanaUser(String securityAction, TransportRequest request, TransportChannel transportChannel,
@@ -220,9 +229,9 @@ class ClientProfile extends NodeProfile {
220229

221230
ClientProfile(AuthenticationService authcService, AuthorizationService authzService,
222231
ThreadContext threadContext, boolean extractClientCert, DestructiveOperations destructiveOperations,
223-
boolean reservedRealmEnabled, SecurityContext securityContext) {
232+
boolean reservedRealmEnabled, SecurityContext securityContext, XPackLicenseState licenseState) {
224233
super(authcService, authzService, threadContext, extractClientCert, destructiveOperations, reservedRealmEnabled,
225-
securityContext);
234+
securityContext, licenseState);
226235
}
227236

228237
@Override

0 commit comments

Comments
 (0)