Log warning when unlicensed realms are skipped (#41828)

Because realms are configured at node startup, but license levels can
change dynamically, it is possible to have a running node that has a
particular realm type configured, but that realm is not permitted under
the current license.
In this case the realm is silently ignored during authentication.

This commit adds a warning in the elasticsearch logs if authentication
fails, and there are realms that have been skipped due to licensing.
This message is not intended to imply that the realms could (or would)
have successfully authenticated the user, but they may help reduce
confusion about why authentication failed if the caller was expecting
the authentication to be handled by a particular realm that is in fact
unlicensed.

Backport of: #41778
This commit is contained in:
Tim Vernum 2019-05-07 09:55:48 +10:00 committed by GitHub
parent a90aac1c71
commit 3508b6c641
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 123 additions and 18 deletions

View File

@ -14,6 +14,7 @@ import org.elasticsearch.Version;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ContextPreservingActionListener;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.cache.Cache;
import org.elasticsearch.common.cache.CacheBuilder;
import org.elasticsearch.common.collect.Tuple;
@ -35,6 +36,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
import org.elasticsearch.xpack.core.security.authc.AuthenticationServiceField;
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
import org.elasticsearch.xpack.core.security.authc.Realm;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.EmptyAuthorizationInfo;
import org.elasticsearch.xpack.core.security.support.Exceptions;
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
import org.elasticsearch.xpack.core.security.user.SystemUser;
@ -43,7 +45,6 @@ import org.elasticsearch.xpack.security.audit.AuditTrail;
import org.elasticsearch.xpack.security.audit.AuditTrailService;
import org.elasticsearch.xpack.security.audit.AuditUtil;
import org.elasticsearch.xpack.security.authc.support.RealmUserLookup;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.EmptyAuthorizationInfo;
import org.elasticsearch.xpack.security.support.SecurityIndexManager;
import java.util.ArrayList;
@ -484,6 +485,13 @@ public class AuthenticationService {
final String cause = tuple.v2() == null ? "" : " (Caused by " + tuple.v2() + ")";
logger.warn("Authentication to realm {} failed - {}{}", realm.name(), message, cause);
});
List<Realm> unlicensedRealms = realms.getUnlicensedRealms();
if (unlicensedRealms.isEmpty() == false) {
logger.warn("Authentication failed using realms [{}]." +
" Realms [{}] were skipped because they are not permitted on the current license",
Strings.collectionToCommaDelimitedString(defaultOrderedRealmList),
Strings.collectionToCommaDelimitedString(unlicensedRealms));
}
listener.onFailure(request.authenticationFailed(authenticationToken));
} else {
threadContext.putTransient(AuthenticationResult.THREAD_CONTEXT_KEY, authenticationResult);

View File

@ -5,24 +5,8 @@
*/
package org.elasticsearch.xpack.security.authc;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.common.collect.MapBuilder;
import org.elasticsearch.common.settings.Settings;
@ -39,6 +23,21 @@ import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings;
import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings;
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
/**
* Serves as a realms registry (also responsible for ordering the realms appropriately)
*/
@ -119,6 +118,32 @@ public class Realms implements Iterable<Realm> {
}
}
/**
* Returns a list of realms that are configured, but are not permitted under the current license.
*/
public List<Realm> getUnlicensedRealms() {
// If auth is not allowed, then everything is unlicensed
if (licenseState.isAuthAllowed() == false) {
return Collections.unmodifiableList(realms);
}
AllowedRealmType allowedRealmType = licenseState.allowedRealmType();
// If all realms are allowed, then nothing is unlicensed
if (allowedRealmType == AllowedRealmType.ALL) {
return Collections.emptyList();
}
final List<Realm> allowedRealms = this.asList();
// Shortcut for the typical case, all the configured realms are allowed
if (allowedRealms.equals(this.realms.size())) {
return Collections.emptyList();
}
// Otherwise, we return anything in "all realms" that is not in the allowed realm list
List<Realm> unlicensed = realms.stream().filter(r -> allowedRealms.contains(r) == false).collect(Collectors.toList());
return Collections.unmodifiableList(unlicensed);
}
public Stream<Realm> stream() {
return StreamSupport.stream(this.spliterator(), false);
}

View File

@ -22,6 +22,7 @@ import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings;
import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings;
import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings;
import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings;
import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings;
import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
@ -39,10 +40,13 @@ import java.util.TreeMap;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.iterableWithSize;
import static org.hamcrest.Matchers.notNullValue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ -104,6 +108,8 @@ public class RealmsTests extends ESTestCase {
assertThat(realm.name(), equalTo("realm_" + index));
i++;
}
assertThat(realms.getUnlicensedRealms(), empty());
}
public void testWithSettingsWhereDifferentRealmsHaveSameOrder() throws Exception {
@ -142,6 +148,8 @@ public class RealmsTests extends ESTestCase {
assertThat(realm.type(), equalTo("type_" + nameToRealmId.get(expectedRealmName)));
assertThat(realm.name(), equalTo(expectedRealmName));
}
assertThat(realms.getUnlicensedRealms(), empty());
}
public void testWithSettingsWithMultipleInternalRealmsOfSameType() throws Exception {
@ -175,6 +183,8 @@ public class RealmsTests extends ESTestCase {
assertThat(realm.type(), equalTo(NativeRealmSettings.TYPE));
assertThat(realm.name(), equalTo("default_" + NativeRealmSettings.TYPE));
assertThat(iter.hasNext(), is(false));
assertThat(realms.getUnlicensedRealms(), empty());
}
public void testUnlicensedWithOnlyCustomRealms() throws Exception {
@ -209,6 +219,8 @@ public class RealmsTests extends ESTestCase {
i++;
}
assertThat(realms.getUnlicensedRealms(), empty());
when(licenseState.allowedRealmType()).thenReturn(AllowedRealmType.DEFAULT);
iter = realms.iterator();
@ -225,6 +237,18 @@ public class RealmsTests extends ESTestCase {
assertThat(realm.name(), equalTo("default_" + NativeRealmSettings.TYPE));
assertThat(iter.hasNext(), is(false));
assertThat(realms.getUnlicensedRealms(), iterableWithSize(randomRealmTypesCount));
iter = realms.getUnlicensedRealms().iterator();
i = 0;
while (iter.hasNext()) {
realm = iter.next();
assertThat(realm.order(), equalTo(i));
int index = orderToIndex.get(i);
assertThat(realm.type(), equalTo("type_" + index));
assertThat(realm.name(), equalTo("realm_" + index));
i++;
}
when(licenseState.allowedRealmType()).thenReturn(AllowedRealmType.NATIVE);
iter = realms.iterator();
@ -240,6 +264,18 @@ public class RealmsTests extends ESTestCase {
assertThat(realm.type(), equalTo(NativeRealmSettings.TYPE));
assertThat(realm.name(), equalTo("default_" + NativeRealmSettings.TYPE));
assertThat(iter.hasNext(), is(false));
assertThat(realms.getUnlicensedRealms(), iterableWithSize(randomRealmTypesCount));
iter = realms.getUnlicensedRealms().iterator();
i = 0;
while (iter.hasNext()) {
realm = iter.next();
assertThat(realm.order(), equalTo(i));
int index = orderToIndex.get(i);
assertThat(realm.type(), equalTo("type_" + index));
assertThat(realm.name(), equalTo("realm_" + index));
i++;
}
}
public void testUnlicensedWithInternalRealms() throws Exception {
@ -266,6 +302,7 @@ public class RealmsTests extends ESTestCase {
types.add(realm.type());
}
assertThat(types, contains("ldap", "type_0"));
assertThat(realms.getUnlicensedRealms(), empty());
when(licenseState.allowedRealmType()).thenReturn(AllowedRealmType.DEFAULT);
iter = realms.iterator();
@ -280,6 +317,11 @@ public class RealmsTests extends ESTestCase {
}
assertThat(i, is(1));
assertThat(realms.getUnlicensedRealms(), iterableWithSize(1));
realm = realms.getUnlicensedRealms().get(0);
assertThat(realm.type(), equalTo("type_0"));
assertThat(realm.name(), equalTo("custom"));
when(licenseState.allowedRealmType()).thenReturn(AllowedRealmType.NATIVE);
iter = realms.iterator();
assertThat(iter.hasNext(), is(true));
@ -294,6 +336,14 @@ public class RealmsTests extends ESTestCase {
assertThat(realm.type(), equalTo(NativeRealmSettings.TYPE));
assertThat(realm.name(), equalTo("default_" + NativeRealmSettings.TYPE));
assertThat(iter.hasNext(), is(false));
assertThat(realms.getUnlicensedRealms(), iterableWithSize(2));
realm = realms.getUnlicensedRealms().get(0);
assertThat(realm.type(), equalTo("ldap"));
assertThat(realm.name(), equalTo("foo"));
realm = realms.getUnlicensedRealms().get(1);
assertThat(realm.type(), equalTo("type_0"));
assertThat(realm.name(), equalTo("custom"));
}
public void testUnlicensedWithNativeRealmSettings() throws Exception {
@ -317,6 +367,7 @@ public class RealmsTests extends ESTestCase {
realm = iter.next();
assertThat(realm.type(), is(type));
assertThat(iter.hasNext(), is(false));
assertThat(realms.getUnlicensedRealms(), empty());
when(licenseState.allowedRealmType()).thenReturn(AllowedRealmType.NATIVE);
iter = realms.iterator();
@ -327,10 +378,15 @@ public class RealmsTests extends ESTestCase {
realm = iter.next();
assertThat(realm.type(), is(type));
assertThat(iter.hasNext(), is(false));
assertThat(realms.getUnlicensedRealms(), iterableWithSize(1));
realm = realms.getUnlicensedRealms().get(0);
assertThat(realm.type(), equalTo("ldap"));
assertThat(realm.name(), equalTo("foo"));
}
public void testUnlicensedWithNonStandardRealms() throws Exception {
final String selectedRealmType = randomFrom(SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE);
final String selectedRealmType = randomFrom(SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE, OpenIdConnectRealmSettings.TYPE);
factories.put(selectedRealmType, config -> new DummyRealm(selectedRealmType, config));
Settings.Builder builder = Settings.builder()
.put("path.home", createTempDir())
@ -346,6 +402,7 @@ public class RealmsTests extends ESTestCase {
realm = iter.next();
assertThat(realm.type(), is(selectedRealmType));
assertThat(iter.hasNext(), is(false));
assertThat(realms.getUnlicensedRealms(), empty());
when(licenseState.allowedRealmType()).thenReturn(AllowedRealmType.DEFAULT);
iter = realms.iterator();
@ -360,6 +417,11 @@ public class RealmsTests extends ESTestCase {
assertThat(realm.type(), is(NativeRealmSettings.TYPE));
assertThat(iter.hasNext(), is(false));
assertThat(realms.getUnlicensedRealms(), iterableWithSize(1));
realm = realms.getUnlicensedRealms().get(0);
assertThat(realm.type(), equalTo(selectedRealmType));
assertThat(realm.name(), equalTo("foo"));
when(licenseState.allowedRealmType()).thenReturn(AllowedRealmType.NATIVE);
iter = realms.iterator();
assertThat(iter.hasNext(), is(true));
@ -372,6 +434,11 @@ public class RealmsTests extends ESTestCase {
realm = iter.next();
assertThat(realm.type(), is(NativeRealmSettings.TYPE));
assertThat(iter.hasNext(), is(false));
assertThat(realms.getUnlicensedRealms(), iterableWithSize(1));
realm = realms.getUnlicensedRealms().get(0);
assertThat(realm.type(), equalTo(selectedRealmType));
assertThat(realm.name(), equalTo("foo"));
}
public void testDisabledRealmsAreNotAdded() throws Exception {
@ -422,6 +489,11 @@ public class RealmsTests extends ESTestCase {
}
assertThat(count, equalTo(orderToIndex.size()));
assertThat(realms.getUnlicensedRealms(), empty());
// check that disabled realms are not included in unlicensed realms
when(licenseState.allowedRealmType()).thenReturn(AllowedRealmType.NATIVE);
assertThat(realms.getUnlicensedRealms(), hasSize(orderToIndex.size()));
}
public void testAuthcAuthzDisabled() throws Exception {