Skip to content

Commit

Permalink
Make authz perf log msg configurable and disabled by default (#82648) (
Browse files Browse the repository at this point in the history
…#82767)

* Make authz perf log msg configurable and disabled by default (#82648)

In #75439 we added log messages for authorization performance.

Real world experience indicates that some clusters generate these log
messages even though the overall cluster performance is acceptable to
the administrators, and the security (RBAC) configuration is within our
recommendations.

This commit changes the threshold levels for these messages by: -
Increasing the default time values for each log level - Making the
levels configurable - Disabling the log by default - Adding a dynamic
setting to enable the logging if required

* Fix compile issues from backport

Co-authored-by: Tim Vernum <tim.vernum@elastic.co>
Co-authored-by: Tim Vernum <tim@adjective.org>
  • Loading branch information
3 people committed Jan 18, 2022
1 parent 3ccdcbe commit c5bc0ac
Show file tree
Hide file tree
Showing 4 changed files with 407 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

package org.elasticsearch.xpack.security.authz;

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.ElasticsearchException;
Expand Down Expand Up @@ -90,7 +89,6 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;

Expand Down Expand Up @@ -133,6 +131,7 @@ public class AuthorizationService {
private final Set<RequestInterceptor> requestInterceptors;
private final XPackLicenseState licenseState;
private final OperatorPrivilegesService operatorPrivilegesService;
private final LoadAuthorizedIndicesTimeChecker.Factory authzIndicesTimerFactory;
private final boolean isAnonymousEnabled;
private final boolean anonymousAuthzExceptionEnabled;

Expand Down Expand Up @@ -164,6 +163,7 @@ public AuthorizationService(
this.settings = settings;
this.licenseState = licenseState;
this.operatorPrivilegesService = operatorPrivilegesService;
this.authzIndicesTimerFactory = new LoadAuthorizedIndicesTimeChecker.Factory(logger, settings, clusterService.getClusterSettings());
}

public void checkPrivileges(
Expand Down Expand Up @@ -407,13 +407,13 @@ private void authorizeAction(
} else if (isIndexAction(action)) {
final Metadata metadata = clusterService.state().metadata();
final AsyncSupplier<Set<String>> authorizedIndicesSupplier = new CachingAsyncSupplier<>(authzIndicesListener -> {
LoadAuthorizedIndiciesTimeChecker timeChecker = LoadAuthorizedIndiciesTimeChecker.start(requestInfo, authzInfo);
Consumer<Collection<String>> timeChecker = authzIndicesTimerFactory.newTimer(requestInfo);
authzEngine.loadAuthorizedIndices(
requestInfo,
authzInfo,
metadata.getIndicesLookup(),
authzIndicesListener.map(authzIndices -> {
timeChecker.done(authzIndices);
timeChecker.accept(authzIndices);
return authzIndices;
})
);
Expand Down Expand Up @@ -1016,58 +1016,8 @@ public void getAsync(ActionListener<V> listener) {
}
}

static class LoadAuthorizedIndiciesTimeChecker {
private static final int WARN_THRESHOLD_MS = 200;
private static final int INFO_THRESHOLD_MS = 50;
private static final int DEBUG_THRESHOLD_MS = 10;

private final long startNanos;
private final RequestInfo requestInfo;

LoadAuthorizedIndiciesTimeChecker(long startNanos, RequestInfo requestInfo) {
this.startNanos = startNanos;
this.requestInfo = requestInfo;
}

public static LoadAuthorizedIndiciesTimeChecker start(RequestInfo requestInfo, AuthorizationInfo authzInfo) {
return new LoadAuthorizedIndiciesTimeChecker(System.nanoTime(), requestInfo);
}

public void done(Collection<String> indices) {
final long end = System.nanoTime();
final long millis = TimeUnit.NANOSECONDS.toMillis(end - startNanos);
if (millis > WARN_THRESHOLD_MS) {
logger.warn(
"Resolving [{}] indices for action [{}] and user [{}] took [{}ms] which is greater than the threshold of {}ms;"
+ " The index privileges for this user may be too complex for this cluster.",
indices.size(),
requestInfo.getAction(),
requestInfo.getAuthentication().getUser().principal(),
millis,
WARN_THRESHOLD_MS
);
} else {
final Level level;
if (millis > INFO_THRESHOLD_MS) {
level = Level.INFO;
} else if (millis > DEBUG_THRESHOLD_MS) {
level = Level.DEBUG;
} else {
level = Level.TRACE;
}
logger.log(
level,
"Took [{}ms] to resolve [{}] indices for action [{}] and user [{}]",
millis,
indices.size(),
requestInfo.getAction(),
requestInfo.getAuthentication().getUser().principal()
);
}
}
}

public static void addSettings(List<Setting<?>> settings) {
settings.add(ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING);
settings.addAll(LoadAuthorizedIndicesTimeChecker.Factory.getSettings());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.security.authz;

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.settings.SettingsException;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;

import java.util.Collection;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

/**
* Checks the time spent evaluating authorized indices for a request.
* Has configurable logging levels based on execution time.
*/
class LoadAuthorizedIndicesTimeChecker implements Consumer<Collection<String>> {

private final Logger logger;
private final long startNanos;
private final AuthorizationEngine.RequestInfo requestInfo;
private final Thresholds thresholds;

LoadAuthorizedIndicesTimeChecker(Logger logger, long startNanos, AuthorizationEngine.RequestInfo requestInfo, Thresholds thresholds) {
this.logger = logger;
this.startNanos = startNanos;
this.requestInfo = requestInfo;
this.thresholds = thresholds;
}

@Override
public void accept(Collection<String> indices) {
final long end = System.nanoTime();
final long millis = TimeUnit.NANOSECONDS.toMillis(end - startNanos);
final Level level = thresholds.getLogLevel(millis);
if (level == Level.WARN) {
logger.warn(
"Resolving [{}] indices for action [{}] and user [{}] took [{}ms] which is greater than the threshold of {}ms;"
+ " The index privileges for this user may be too complex for this cluster.",
indices.size(),
requestInfo.getAction(),
requestInfo.getAuthentication().getUser().principal(),
millis,
thresholds.warnThresholdMs
);
} else {
logger.log(
level,
"Took [{}ms] to resolve [{}] indices for action [{}] and user [{}]",
millis,
indices.size(),
requestInfo.getAction(),
requestInfo.getAuthentication().getUser().principal()
);
}
}

static final Setting<Boolean> LOGGING_ENABLED_SETTING = Setting.boolSetting(
"xpack.security.authz.timer.indices.enabled",
false,
Setting.Property.NodeScope,
Setting.Property.Dynamic
);
static final Setting<TimeValue> DEBUG_THRESHOLD_SETTING = Setting.timeSetting(
"xpack.security.authz.timer.indices.threshold.debug",
TimeValue.timeValueMillis(20),
Setting.Property.NodeScope
);
static final Setting<TimeValue> INFO_THRESHOLD_SETTING = Setting.timeSetting(
"xpack.security.authz.timer.indices.threshold.info",
TimeValue.timeValueMillis(100),
Setting.Property.NodeScope
);
static final Setting<TimeValue> WARN_THRESHOLD_SETTING = Setting.timeSetting(
"xpack.security.authz.timer.indices.threshold.warn",
TimeValue.timeValueMillis(200),
Setting.Property.NodeScope
);

static class Thresholds {
private final long debugThresholdMs;
private final long infoThresholdMs;
private final long warnThresholdMs;

Thresholds(TimeValue debugThreshold, TimeValue infoThreshold, TimeValue warnThreshold) {
this.debugThresholdMs = debugThreshold.millis();
this.infoThresholdMs = infoThreshold.millis();
this.warnThresholdMs = warnThreshold.millis();
}

public Level getLogLevel(long millis) {
if (millis > warnThresholdMs) {
return Level.WARN;
}
if (millis > infoThresholdMs) {
return Level.INFO;
}
if (millis > debugThresholdMs) {
return Level.DEBUG;
}
return Level.TRACE;
}

long getDebugThresholdMs() {
return debugThresholdMs;
}

long getInfoThresholdMs() {
return infoThresholdMs;
}

long getWarnThresholdMs() {
return warnThresholdMs;
}
}

static final Consumer<Collection<String>> NO_OP_CONSUMER = ignore -> {};

static class Factory {
private final Logger logger;
private volatile boolean loggingEnabled;
private final Thresholds thresholds;

Factory(Logger logger, Settings settings, ClusterSettings clusterSettings) {
this.logger = logger;
this.loggingEnabled = LOGGING_ENABLED_SETTING.get(settings);
clusterSettings.addSettingsUpdateConsumer(LOGGING_ENABLED_SETTING, enabled -> this.loggingEnabled = enabled);

TimeValue debugThreshold = DEBUG_THRESHOLD_SETTING.get(settings);
TimeValue infoThreshold = INFO_THRESHOLD_SETTING.get(settings);
TimeValue warnThreshold = WARN_THRESHOLD_SETTING.get(settings);

if (infoThreshold.compareTo(debugThreshold) < 0) {
throw new SettingsException(
"Setting [{}] ({}) cannot be less than the setting [{}] ({})",
INFO_THRESHOLD_SETTING.getKey(),
infoThreshold,
DEBUG_THRESHOLD_SETTING.getKey(),
debugThreshold
);
}
if (warnThreshold.compareTo(infoThreshold) < 0) {
throw new SettingsException(
"Setting [{}] ({}) cannot be less than the setting [{}] ({})",
WARN_THRESHOLD_SETTING.getKey(),
warnThreshold,
INFO_THRESHOLD_SETTING.getKey(),
infoThreshold
);
}

this.thresholds = new Thresholds(debugThreshold, infoThreshold, warnThreshold);
}

public static Set<Setting<?>> getSettings() {
return org.elasticsearch.core.Set.of(
LOGGING_ENABLED_SETTING,
DEBUG_THRESHOLD_SETTING,
INFO_THRESHOLD_SETTING,
WARN_THRESHOLD_SETTING
);
}

public Consumer<Collection<String>> newTimer(AuthorizationEngine.RequestInfo requestInfo) {
if (loggingEnabled) {
return new LoadAuthorizedIndicesTimeChecker(logger, System.nanoTime(), requestInfo, thresholds);
} else {
return NO_OP_CONSUMER;
}
}

public Thresholds getThresholds() {
return thresholds;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
*/
package org.elasticsearch.xpack.security.authz;

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionListener;
Expand Down Expand Up @@ -96,11 +93,11 @@
import org.elasticsearch.common.TriFunction;
import org.elasticsearch.common.UUIDs;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.common.util.concurrent.ThreadContext.StoredContext;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.core.Tuple;
import org.elasticsearch.index.IndexNotFoundException;
Expand All @@ -113,7 +110,6 @@
import org.elasticsearch.search.internal.AliasFilter;
import org.elasticsearch.search.internal.ShardSearchRequest;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.MockLogAppender;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportActionProxy;
import org.elasticsearch.transport.TransportRequest;
Expand Down Expand Up @@ -184,11 +180,9 @@
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;

import static java.util.Arrays.asList;
import static java.util.Collections.singletonMap;
Expand Down Expand Up @@ -243,7 +237,10 @@ public void setup() {
rolesStore = mock(CompositeRolesStore.class);
clusterService = mock(ClusterService.class);
final Settings settings = Settings.builder().put("cluster.remote.other_cluster.seeds", "localhost:9999").build();
final ClusterSettings clusterSettings = new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS);
final ClusterSettings clusterSettings = new ClusterSettings(
settings,
Sets.union(ClusterSettings.BUILT_IN_CLUSTER_SETTINGS, LoadAuthorizedIndicesTimeChecker.Factory.getSettings())
);
when(clusterService.getClusterSettings()).thenReturn(clusterSettings);
when(clusterService.state()).thenReturn(ClusterState.EMPTY_STATE);
auditTrail = mock(AuditTrail.class);
Expand Down Expand Up @@ -2772,46 +2769,6 @@ public void testOperatorPrivileges() {
verifyNoMoreInteractions(auditTrail);
}

public void testAuthorizedIndiciesTimeChecker() throws Exception {
final Authentication authentication = createAuthentication(new User("slow-user", "slow-role"));
final long now = System.nanoTime();
final AuthorizationEngine.RequestInfo requestInfo = new AuthorizationEngine.RequestInfo(
authentication,
new SearchRequest(),
SearchAction.NAME,
null
);
final AuthorizationService.LoadAuthorizedIndiciesTimeChecker checker = new AuthorizationService.LoadAuthorizedIndiciesTimeChecker(
now - TimeUnit.MILLISECONDS.toNanos(210),
requestInfo
);
final Logger serviceLogger = LogManager.getLogger(AuthorizationService.class);
final MockLogAppender mockAppender = new MockLogAppender();
mockAppender.start();
try {
Loggers.addAppender(serviceLogger, mockAppender);

mockAppender.addExpectation(
new MockLogAppender.PatternSeenEventExpectation(
"WARN-Slow Index Resolution",
serviceLogger.getName(),
Level.WARN,
Pattern.quote("Resolving [0] indices for action [" + SearchAction.NAME + "] and user [slow-user] took [")
+ "\\d{3}"
+ Pattern.quote(
"ms] which is greater than the threshold of 200ms;"
+ " The index privileges for this user may be too complex for this cluster."
)
)
);
checker.done(Collections.emptyList());
mockAppender.assertAllExpectationsMatched();
} finally {
Loggers.removeAppender(serviceLogger, mockAppender);
mockAppender.stop();
}
}

static AuthorizationInfo authzInfoRoles(String[] expectedRoles) {
return ArgumentMatchers.argThat(new RBACAuthorizationInfoRoleMatcher(expectedRoles));
}
Expand Down

0 comments on commit c5bc0ac

Please sign in to comment.