Security: make native realm usage stats accurate (#30824)

The native realm's usage stats were previously pulled from the cache,
which only contains the number of users that had authenticated in the
past 20 minutes. This commit changes this so that we pull the current
value from the security index by executing a search request. In order
to support this, the usage stats for realms is now asynchronous so that
we do not block while waiting on the search to complete.
This commit is contained in:
Jay Modi 2018-06-06 08:18:56 -06:00 committed by GitHub
parent f4a412fe21
commit 8aa58887e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 240 additions and 119 deletions

View File

@ -92,4 +92,8 @@ public class SecurityFeatureSetUsage extends XPackFeatureSet.Usage {
builder.field(ANONYMOUS_XFIELD, anonymousUsage);
}
}
public Map<String, Object> getRealmsUsage() {
return Collections.unmodifiableMap(realmsUsage);
}
}

View File

@ -119,11 +119,11 @@ public abstract class Realm implements Comparable<Realm> {
*/
public abstract void lookupUser(String username, ActionListener<User> listener);
public Map<String, Object> usageStats() {
public void usageStats(ActionListener<Map<String, Object>> listener) {
Map<String, Object> stats = new HashMap<>();
stats.put("name", name());
stats.put("order", order());
return stats;
listener.onResponse(stats);
}
@Override

View File

@ -86,7 +86,6 @@ public class SecurityFeatureSet implements XPackFeatureSet {
@Override
public void usage(ActionListener<XPackFeatureSet.Usage> listener) {
Map<String, Object> realmsUsage = buildRealmsUsage(realms);
Map<String, Object> sslUsage = sslUsage(settings);
Map<String, Object> auditUsage = auditUsage(settings);
Map<String, Object> ipFilterUsage = ipFilterUsage(ipFilter);
@ -94,10 +93,11 @@ public class SecurityFeatureSet implements XPackFeatureSet {
final AtomicReference<Map<String, Object>> rolesUsageRef = new AtomicReference<>();
final AtomicReference<Map<String, Object>> roleMappingUsageRef = new AtomicReference<>();
final CountDown countDown = new CountDown(2);
final AtomicReference<Map<String, Object>> realmsUsageRef = new AtomicReference<>();
final CountDown countDown = new CountDown(3);
final Runnable doCountDown = () -> {
if (countDown.countDown()) {
listener.onResponse(new SecurityFeatureSetUsage(available(), enabled(), realmsUsage,
listener.onResponse(new SecurityFeatureSetUsage(available(), enabled(), realmsUsageRef.get(),
rolesUsageRef.get(), roleMappingUsageRef.get(),
sslUsage, auditUsage, ipFilterUsage, anonymousUsage));
}
@ -116,6 +116,12 @@ public class SecurityFeatureSet implements XPackFeatureSet {
doCountDown.run();
}, listener::onFailure);
final ActionListener<Map<String, Object>> realmsUsageListener =
ActionListener.wrap(realmsUsage -> {
realmsUsageRef.set(realmsUsage);
doCountDown.run();
}, listener::onFailure);
if (rolesStore == null) {
rolesStoreUsageListener.onResponse(Collections.emptyMap());
} else {
@ -126,13 +132,11 @@ public class SecurityFeatureSet implements XPackFeatureSet {
} else {
roleMappingStore.usageStats(roleMappingStoreUsageListener);
}
}
static Map<String, Object> buildRealmsUsage(Realms realms) {
if (realms == null) {
return Collections.emptyMap();
realmsUsageListener.onResponse(Collections.emptyMap());
} else {
realms.usageStats(realmsUsageListener);
}
return realms.usageStats();
}
static Map<String, Object> sslUsage(Settings settings) {

View File

@ -15,12 +15,16 @@ 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.elasticsearch.action.ActionListener;
import org.elasticsearch.common.collect.MapBuilder;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.CountDown;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.env.Environment;
import org.elasticsearch.license.XPackLicenseState;
@ -188,46 +192,67 @@ public class Realms extends AbstractComponent implements Iterable<Realm> {
return realms;
}
public Map<String, Object> usageStats() {
public void usageStats(ActionListener<Map<String, Object>> listener) {
Map<String, Object> realmMap = new HashMap<>();
for (Realm realm : this) {
if (ReservedRealm.TYPE.equals(realm.type())) {
continue;
final AtomicBoolean failed = new AtomicBoolean(false);
final List<Realm> realmList = asList().stream()
.filter(r -> ReservedRealm.TYPE.equals(r.type()) == false)
.collect(Collectors.toList());
final CountDown countDown = new CountDown(realmList.size());
final Runnable doCountDown = () -> {
if ((realmList.isEmpty() || countDown.countDown()) && failed.get() == false) {
final AllowedRealmType allowedRealmType = licenseState.allowedRealmType();
// iterate over the factories so we can add enabled & available info
for (String type : factories.keySet()) {
assert ReservedRealm.TYPE.equals(type) == false;
realmMap.compute(type, (key, value) -> {
if (value == null) {
return MapBuilder.<String, Object>newMapBuilder()
.put("enabled", false)
.put("available", isRealmTypeAvailable(allowedRealmType, type))
.map();
}
assert value instanceof Map;
Map<String, Object> realmTypeUsage = (Map<String, Object>) value;
realmTypeUsage.put("enabled", true);
// the realms iterator returned this type so it must be enabled
assert isRealmTypeAvailable(allowedRealmType, type);
realmTypeUsage.put("available", true);
return value;
});
}
listener.onResponse(realmMap);
}
};
if (realmList.isEmpty()) {
doCountDown.run();
} else {
for (Realm realm : realmList) {
realm.usageStats(ActionListener.wrap(stats -> {
if (failed.get() == false) {
synchronized (realmMap) {
realmMap.compute(realm.type(), (key, value) -> {
if (value == null) {
Object realmTypeUsage = convertToMapOfLists(stats);
return realmTypeUsage;
}
assert value instanceof Map;
combineMaps((Map<String, Object>) value, stats);
return value;
});
}
doCountDown.run();
}
},
e -> {
if (failed.compareAndSet(false, true)) {
listener.onFailure(e);
}
}));
}
realmMap.compute(realm.type(), (key, value) -> {
if (value == null) {
Object realmTypeUsage = convertToMapOfLists(realm.usageStats());
return realmTypeUsage;
}
assert value instanceof Map;
combineMaps((Map<String, Object>) value, realm.usageStats());
return value;
});
}
final AllowedRealmType allowedRealmType = licenseState.allowedRealmType();
// iterate over the factories so we can add enabled & available info
for (String type : factories.keySet()) {
assert ReservedRealm.TYPE.equals(type) == false;
realmMap.compute(type, (key, value) -> {
if (value == null) {
return MapBuilder.<String, Object>newMapBuilder()
.put("enabled", false)
.put("available", isRealmTypeAvailable(allowedRealmType, type))
.map();
}
assert value instanceof Map;
Map<String, Object> realmTypeUsage = (Map<String, Object>) value;
realmTypeUsage.put("enabled", true);
// the realms iterator returned this type so it must be enabled
assert isRealmTypeAvailable(allowedRealmType, type);
realmTypeUsage.put("available", true);
return value;
});
}
return realmMap;
}
private void addNativeRealms(List<Realm> realms) throws Exception {

View File

@ -15,6 +15,8 @@ import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRealm;
import org.elasticsearch.xpack.security.support.SecurityIndexManager;
import java.util.Map;
import static org.elasticsearch.xpack.security.support.SecurityIndexManager.isIndexDeleted;
import static org.elasticsearch.xpack.security.support.SecurityIndexManager.isMoveFromRedToNonRed;
@ -46,6 +48,16 @@ public class NativeRealm extends CachingUsernamePasswordRealm {
}
}
@Override
public void usageStats(ActionListener<Map<String, Object>> listener) {
super.usageStats(ActionListener.wrap(stats ->
userStore.getUserCount(ActionListener.wrap(size -> {
stats.put("size", size);
listener.onResponse(stats);
}, listener::onFailure))
, listener::onFailure));
}
// method is used for testing to verify cache expiration since expireAll is final
void clearCache() {
expireAll();

View File

@ -150,6 +150,30 @@ public class NativeUsersStore extends AbstractComponent {
}
}
void getUserCount(final ActionListener<Long> listener) {
if (securityIndex.indexExists() == false) {
listener.onResponse(0L);
} else {
securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () ->
executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN,
client.prepareSearch(SECURITY_INDEX_NAME)
.setQuery(QueryBuilders.termQuery(Fields.TYPE.getPreferredName(), USER_DOC_TYPE))
.setSize(0)
.request(),
new ActionListener<SearchResponse>() {
@Override
public void onResponse(SearchResponse response) {
listener.onResponse(response.getHits().getTotalHits());
}
@Override
public void onFailure(Exception e) {
listener.onFailure(e);
}
}, client::search));
}
}
/**
* Async method to retrieve a user and their password
*/

View File

@ -55,11 +55,11 @@ public class FileRealm extends CachingUsernamePasswordRealm {
}
@Override
public Map<String, Object> usageStats() {
Map<String, Object> stats = super.usageStats();
// here we can determine the size based on the in mem user store
stats.put("size", userPasswdStore.usersCount());
return stats;
public void usageStats(ActionListener<Map<String, Object>> listener) {
super.usageStats(ActionListener.wrap(stats -> {
stats.put("size", userPasswdStore.usersCount());
listener.onResponse(stats);
}, listener::onFailure));
}
}

View File

@ -160,12 +160,14 @@ public final class LdapRealm extends CachingUsernamePasswordRealm {
}
@Override
public Map<String, Object> usageStats() {
Map<String, Object> usage = super.usageStats();
usage.put("load_balance_type", LdapLoadBalancing.resolve(config.settings()).toString());
usage.put("ssl", sessionFactory.isSslUsed());
usage.put("user_search", LdapUserSearchSessionFactory.hasUserSearchSettings(config));
return usage;
public void usageStats(ActionListener<Map<String, Object>> listener) {
super.usageStats(ActionListener.wrap(usage -> {
usage.put("size", getCacheSize());
usage.put("load_balance_type", LdapLoadBalancing.resolve(config.settings()).toString());
usage.put("ssl", sessionFactory.isSslUsed());
usage.put("user_search", LdapUserSearchSessionFactory.hasUserSearchSettings(config));
listener.onResponse(usage);
}, listener::onFailure));
}
private static void buildUser(LdapSession session, String username, ActionListener<AuthenticationResult> listener,

View File

@ -22,6 +22,7 @@ import org.elasticsearch.xpack.core.security.authc.support.Hasher;
import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
import org.elasticsearch.xpack.core.security.user.User;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
@ -177,10 +178,15 @@ public abstract class CachingUsernamePasswordRealm extends UsernamePasswordRealm
}
@Override
public Map<String, Object> usageStats() {
Map<String, Object> stats = super.usageStats();
stats.put("size", cache.count());
return stats;
public void usageStats(ActionListener<Map<String, Object>> listener) {
super.usageStats(ActionListener.wrap(stats -> {
stats.put("cache", Collections.singletonMap("size", getCacheSize()));
listener.onResponse(stats);
}, listener::onFailure));
}
protected int getCacheSize() {
return cache.count();
}
protected abstract void doAuthenticate(UsernamePasswordToken token, ActionListener<AuthenticationResult> listener);

View File

@ -87,7 +87,7 @@ public class FileRolesStore extends AbstractComponent {
}
public Map<String, Object> usageStats() {
Map<String, Object> usageStats = new HashMap<>();
Map<String, Object> usageStats = new HashMap<>(3);
usageStats.put("size", permissions.size());
boolean dls = false;

View File

@ -195,7 +195,7 @@ public class NativeRolesStore extends AbstractComponent {
}
public void usageStats(ActionListener<Map<String, Object>> listener) {
Map<String, Object> usageStats = new HashMap<>();
Map<String, Object> usageStats = new HashMap<>(3);
if (securityIndex.indexExists() == false) {
usageStats.put("size", 0L);
usageStats.put("fls", false);
@ -204,56 +204,56 @@ public class NativeRolesStore extends AbstractComponent {
} else {
securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () ->
executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN,
client.prepareMultiSearch()
.add(client.prepareSearch(SecurityIndexManager.SECURITY_INDEX_NAME)
.setQuery(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE))
.setSize(0))
.add(client.prepareSearch(SecurityIndexManager.SECURITY_INDEX_NAME)
.setQuery(QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE))
.must(QueryBuilders.boolQuery()
.should(existsQuery("indices.field_security.grant"))
.should(existsQuery("indices.field_security.except"))
// for backwardscompat with 2.x
.should(existsQuery("indices.fields"))))
.setSize(0)
.setTerminateAfter(1))
.add(client.prepareSearch(SecurityIndexManager.SECURITY_INDEX_NAME)
.setQuery(QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE))
.filter(existsQuery("indices.query")))
.setSize(0)
.setTerminateAfter(1))
.request(),
new ActionListener<MultiSearchResponse>() {
@Override
public void onResponse(MultiSearchResponse items) {
Item[] responses = items.getResponses();
if (responses[0].isFailure()) {
usageStats.put("size", 0);
} else {
usageStats.put("size", responses[0].getResponse().getHits().getTotalHits());
}
if (responses[1].isFailure()) {
usageStats.put("fls", false);
} else {
usageStats.put("fls", responses[1].getResponse().getHits().getTotalHits() > 0L);
}
if (responses[2].isFailure()) {
usageStats.put("dls", false);
} else {
usageStats.put("dls", responses[2].getResponse().getHits().getTotalHits() > 0L);
}
listener.onResponse(usageStats);
client.prepareMultiSearch()
.add(client.prepareSearch(SecurityIndexManager.SECURITY_INDEX_NAME)
.setQuery(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE))
.setSize(0))
.add(client.prepareSearch(SecurityIndexManager.SECURITY_INDEX_NAME)
.setQuery(QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE))
.must(QueryBuilders.boolQuery()
.should(existsQuery("indices.field_security.grant"))
.should(existsQuery("indices.field_security.except"))
// for backwardscompat with 2.x
.should(existsQuery("indices.fields"))))
.setSize(0)
.setTerminateAfter(1))
.add(client.prepareSearch(SecurityIndexManager.SECURITY_INDEX_NAME)
.setQuery(QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE))
.filter(existsQuery("indices.query")))
.setSize(0)
.setTerminateAfter(1))
.request(),
new ActionListener<MultiSearchResponse>() {
@Override
public void onResponse(MultiSearchResponse items) {
Item[] responses = items.getResponses();
if (responses[0].isFailure()) {
usageStats.put("size", 0);
} else {
usageStats.put("size", responses[0].getResponse().getHits().getTotalHits());
}
@Override
public void onFailure(Exception e) {
listener.onFailure(e);
if (responses[1].isFailure()) {
usageStats.put("fls", false);
} else {
usageStats.put("fls", responses[1].getResponse().getHits().getTotalHits() > 0L);
}
}, client::multiSearch));
if (responses[2].isFailure()) {
usageStats.put("dls", false);
} else {
usageStats.put("dls", responses[2].getResponse().getHits().getTotalHits() > 0L);
}
listener.onResponse(usageStats);
}
@Override
public void onFailure(Exception e) {
listener.onFailure(e);
}
}, client::multiSearch));
}
}

View File

@ -146,7 +146,11 @@ public class SecurityFeatureSetTests extends ESTestCase {
realmUsage.put("key2", Arrays.asList(i));
realmUsage.put("key3", Arrays.asList(i % 2 == 0));
}
when(realms.usageStats()).thenReturn(realmsUsageStats);
doAnswer(invocationOnMock -> {
ActionListener<Map<String, Object>> listener = (ActionListener) invocationOnMock.getArguments()[0];
listener.onResponse(realmsUsageStats);
return Void.TYPE;
}).when(realms).usageStats(any(ActionListener.class));
final boolean anonymousEnabled = randomBoolean();
if (anonymousEnabled) {

View File

@ -6,6 +6,7 @@
package org.elasticsearch.xpack.security.authc;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.env.Environment;
@ -454,9 +455,11 @@ public class RealmsTests extends ESTestCase {
.put("xpack.security.authc.realms.bar.order", "1");
Settings settings = builder.build();
Environment env = TestEnvironment.newEnvironment(settings);
Realms realms = new Realms(settings, env, factories, licenseState, threadContext, reservedRealm );
Realms realms = new Realms(settings, env, factories, licenseState, threadContext, reservedRealm);
Map<String, Object> usageStats = realms.usageStats();
PlainActionFuture<Map<String, Object>> future = new PlainActionFuture<>();
realms.usageStats(future);
Map<String, Object> usageStats = future.get();
assertThat(usageStats.size(), is(factories.size()));
// first check type_0
@ -482,7 +485,9 @@ public class RealmsTests extends ESTestCase {
// disable ALL using license
when(licenseState.isAuthAllowed()).thenReturn(false);
when(licenseState.allowedRealmType()).thenReturn(AllowedRealmType.NONE);
usageStats = realms.usageStats();
future = new PlainActionFuture<>();
realms.usageStats(future);
usageStats = future.get();
assertThat(usageStats.size(), is(factories.size()));
for (Entry<String, Object> entry : usageStats.entrySet()) {
Map<String, Object> typeMap = (Map<String, Object>) entry.getValue();
@ -494,7 +499,9 @@ public class RealmsTests extends ESTestCase {
// check native or internal realms enabled only
when(licenseState.isAuthAllowed()).thenReturn(true);
when(licenseState.allowedRealmType()).thenReturn(randomFrom(AllowedRealmType.NATIVE, AllowedRealmType.DEFAULT));
usageStats = realms.usageStats();
future = new PlainActionFuture<>();
realms.usageStats(future);
usageStats = future.get();
assertThat(usageStats.size(), is(factories.size()));
for (Entry<String, Object> entry : usageStats.entrySet()) {
final String type = entry.getKey();

View File

@ -22,6 +22,10 @@ import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.test.NativeRealmIntegTestCase;
import org.elasticsearch.test.SecuritySettingsSource;
import org.elasticsearch.test.SecuritySettingsSourceField;
import org.elasticsearch.xpack.core.XPackFeatureSet;
import org.elasticsearch.xpack.core.action.XPackUsageRequestBuilder;
import org.elasticsearch.xpack.core.action.XPackUsageResponse;
import org.elasticsearch.xpack.core.security.SecurityFeatureSetUsage;
import org.elasticsearch.xpack.core.security.action.role.DeleteRoleResponse;
import org.elasticsearch.xpack.core.security.action.role.GetRolesResponse;
import org.elasticsearch.xpack.core.security.action.role.PutRoleResponse;
@ -49,6 +53,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE;
@ -662,6 +667,28 @@ public class NativeRealmIntegTests extends NativeRealmIntegTestCase {
assertThat(usage.get("dls"), is(dls));
}
public void testRealmUsageStats() {
final int numNativeUsers = scaledRandomIntBetween(1, 32);
SecurityClient securityClient = new SecurityClient(client());
for (int i = 0; i < numNativeUsers; i++) {
securityClient.preparePutUser("joe" + i, "s3krit".toCharArray(), "superuser").get();
}
XPackUsageResponse response = new XPackUsageRequestBuilder(client()).get();
Optional<XPackFeatureSet.Usage> securityUsage = response.getUsages().stream()
.filter(usage -> usage instanceof SecurityFeatureSetUsage)
.findFirst();
assertTrue(securityUsage.isPresent());
SecurityFeatureSetUsage securityFeatureSetUsage = (SecurityFeatureSetUsage) securityUsage.get();
Map<String, Object> realmsUsage = securityFeatureSetUsage.getRealmsUsage();
assertNotNull(realmsUsage);
assertNotNull(realmsUsage.get("native"));
assertNotNull(((Map<String, Object>) realmsUsage.get("native")).get("size"));
List<Long> sizeList = (List<Long>) ((Map<String, Object>) realmsUsage.get("native")).get("size");
assertEquals(1, sizeList.size());
assertEquals(numNativeUsers, Math.toIntExact(sizeList.get(0)));
}
public void testSetEnabled() throws Exception {
securityClient().preparePutUser("joe", "s3krit".toCharArray(), SecuritySettingsSource.TEST_ROLE).get();
final String token = basicAuthHeaderValue("joe", new SecureString("s3krit".toCharArray()));

View File

@ -248,7 +248,9 @@ public class FileRealmTests extends ESTestCase {
threadContext);
FileRealm realm = new FileRealm(config, userPasswdStore, userRolesStore, threadPool);
Map<String, Object> usage = realm.usageStats();
PlainActionFuture<Map<String, Object>> future = new PlainActionFuture<>();
realm.usageStats(future);
Map<String, Object> usage = future.get();
assertThat(usage, is(notNullValue()));
assertThat(usage, hasEntry("name", "file-realm"));
assertThat(usage, hasEntry("order", order));

View File

@ -320,7 +320,9 @@ public class ActiveDirectoryRealmTests extends ESTestCase {
DnRoleMapper roleMapper = new DnRoleMapper(config, resourceWatcherService);
LdapRealm realm = new LdapRealm(LdapRealmSettings.AD_TYPE, config, sessionFactory, roleMapper, threadPool);
Map<String, Object> stats = realm.usageStats();
PlainActionFuture<Map<String, Object>> future = new PlainActionFuture<>();
realm.usageStats(future);
Map<String, Object> stats = future.get();
assertThat(stats, is(notNullValue()));
assertThat(stats, hasEntry("name", realm.name()));
assertThat(stats, hasEntry("order", realm.order()));

View File

@ -360,7 +360,9 @@ public class LdapRealmTests extends LdapTestCase {
LdapRealm realm = new LdapRealm(LdapRealmSettings.LDAP_TYPE, config, ldapFactory,
new DnRoleMapper(config, resourceWatcherService), threadPool);
Map<String, Object> stats = realm.usageStats();
PlainActionFuture<Map<String, Object>> future = new PlainActionFuture<>();
realm.usageStats(future);
Map<String, Object> stats = future.get();
assertThat(stats, is(notNullValue()));
assertThat(stats, hasEntry("name", "ldap-realm"));
assertThat(stats, hasEntry("order", realm.order()));