From f985866b9ba50ad6d689766d80490055c2b44f64 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Fri, 31 May 2019 14:28:25 +1000 Subject: [PATCH] Log the status of security on license change Whether security is enabled/disabled is dependent on the combination of the node settings and the cluster license. This commit adds a license state listener that logs when the license change causes security to switch state (or to be initialised). This is primarily useful for diagnosing cluster formation issues. Backport of: #42488 --- .../elasticsearch/test/MockLogAppender.java | 2 +- .../xpack/security/Security.java | 2 + .../support/SecurityStatusChangeListener.java | 45 +++++++ .../SecurityStatusChangeListenerTests.java | 115 ++++++++++++++++++ 4 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityStatusChangeListener.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/SecurityStatusChangeListenerTests.java diff --git a/test/framework/src/main/java/org/elasticsearch/test/MockLogAppender.java b/test/framework/src/main/java/org/elasticsearch/test/MockLogAppender.java index c6a5d77faf5f3..e9c53ed896765 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/MockLogAppender.java +++ b/test/framework/src/main/java/org/elasticsearch/test/MockLogAppender.java @@ -117,7 +117,7 @@ public UnseenEventExpectation(String name, String logger, Level level, String me @Override public void assertMatched() { - assertThat("expected to see " + name + " but did not", saw, equalTo(false)); + assertThat("expected not to see " + name + " but did", saw, equalTo(false)); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 9ba3bdab21fce..72a9f78006564 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -226,6 +226,7 @@ import org.elasticsearch.xpack.security.rest.action.user.RestPutUserAction; import org.elasticsearch.xpack.security.rest.action.user.RestSetEnabledAction; import org.elasticsearch.xpack.security.support.SecurityIndexManager; +import org.elasticsearch.xpack.security.support.SecurityStatusChangeListener; import org.elasticsearch.xpack.security.transport.SecurityHttpSettings; import org.elasticsearch.xpack.security.transport.SecurityServerTransportInterceptor; import org.elasticsearch.xpack.security.transport.filter.IPFilter; @@ -461,6 +462,7 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste // to keep things simple, just invalidate all cached entries on license change. this happens so rarely that the impact should be // minimal getLicenseState().addListener(allRolesStore::invalidateAll); + getLicenseState().addListener(new SecurityStatusChangeListener(getLicenseState())); final AuthenticationFailureHandler failureHandler = createAuthenticationFailureHandler(realms); authcService.set(new AuthenticationService(settings, realms, auditTrailService, failureHandler, threadPool, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityStatusChangeListener.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityStatusChangeListener.java new file mode 100644 index 0000000000000..ddc41561afabd --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityStatusChangeListener.java @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.support; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.license.LicenseStateListener; +import org.elasticsearch.license.XPackLicenseState; + +import java.util.Objects; + +/** + * A listener for license state changes that provides log messages when a license change + * causes security to switch between enable and disabled (or vice versa). + */ +public class SecurityStatusChangeListener implements LicenseStateListener { + + private final Logger logger; + private final XPackLicenseState licenseState; + private Boolean securityEnabled; + + public SecurityStatusChangeListener(XPackLicenseState licenseState) { + this.logger = LogManager.getLogger(getClass()); + this.licenseState = licenseState; + this.securityEnabled = null; + } + + /** + * This listener will not be registered if security has been explicitly disabled, so we only need to account for dynamic changes due + * to changes in the applied license. + */ + @Override + public synchronized void licenseStateChanged() { + final boolean newState = licenseState.isSecurityAvailable() && licenseState.isSecurityDisabledByLicenseDefaults() == false; + // old state might be null (undefined) so do Object comparison + if (Objects.equals(newState, securityEnabled) == false) { + logger.info("Active license is now [{}]; Security is {}", licenseState.getOperationMode(), newState ? "enabled" : "disabled"); + this.securityEnabled = newState; + } + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/SecurityStatusChangeListenerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/SecurityStatusChangeListenerTests.java new file mode 100644 index 0000000000000..da18d5dc902d4 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/SecurityStatusChangeListenerTests.java @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.support; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.license.License; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.MockLogAppender; +import org.junit.After; +import org.junit.Before; +import org.mockito.Mockito; + +import static org.mockito.Mockito.when; + +public class SecurityStatusChangeListenerTests extends ESTestCase { + + private XPackLicenseState licenseState; + private SecurityStatusChangeListener listener; + private MockLogAppender logAppender; + private Logger listenerLogger; + + @Before + public void setup() throws IllegalAccessException { + licenseState = Mockito.mock(XPackLicenseState.class); + when(licenseState.isSecurityAvailable()).thenReturn(true); + + listener = new SecurityStatusChangeListener(licenseState); + + logAppender = new MockLogAppender(); + logAppender.start(); + listenerLogger = LogManager.getLogger(listener.getClass()); + Loggers.addAppender(listenerLogger, logAppender); + } + + @After + public void cleanup() { + Loggers.removeAppender(listenerLogger, logAppender); + logAppender.stop(); + } + + public void testSecurityEnabledToDisabled() { + when(licenseState.isSecurityDisabledByLicenseDefaults()).thenReturn(false); + + when(licenseState.getOperationMode()).thenReturn(License.OperationMode.GOLD); + logAppender.addExpectation(new MockLogAppender.SeenEventExpectation( + "initial change", + listener.getClass().getName(), + Level.INFO, + "Active license is now [GOLD]; Security is enabled" + )); + listener.licenseStateChanged(); + + when(licenseState.getOperationMode()).thenReturn(License.OperationMode.PLATINUM); + logAppender.addExpectation(new MockLogAppender.UnseenEventExpectation( + "no-op change", + listener.getClass().getName(), + Level.INFO, + "Active license is now [PLATINUM]; Security is enabled" + )); + + when(licenseState.isSecurityDisabledByLicenseDefaults()).thenReturn(true); + when(licenseState.getOperationMode()).thenReturn(License.OperationMode.BASIC); + logAppender.addExpectation(new MockLogAppender.SeenEventExpectation( + "change to basic", + listener.getClass().getName(), + Level.INFO, + "Active license is now [BASIC]; Security is disabled" + )); + listener.licenseStateChanged(); + + logAppender.assertAllExpectationsMatched(); + } + + public void testSecurityDisabledToEnabled() { + when(licenseState.isSecurityDisabledByLicenseDefaults()).thenReturn(true); + + when(licenseState.getOperationMode()).thenReturn(License.OperationMode.TRIAL); + logAppender.addExpectation(new MockLogAppender.SeenEventExpectation( + "initial change", + listener.getClass().getName(), + Level.INFO, + "Active license is now [TRIAL]; Security is disabled" + )); + listener.licenseStateChanged(); + + when(licenseState.getOperationMode()).thenReturn(License.OperationMode.BASIC); + logAppender.addExpectation(new MockLogAppender.UnseenEventExpectation( + "no-op change", + listener.getClass().getName(), + Level.INFO, + "Active license is now [BASIC]; Security is disabled" + )); + + when(licenseState.isSecurityDisabledByLicenseDefaults()).thenReturn(false); + when(licenseState.getOperationMode()).thenReturn(License.OperationMode.PLATINUM); + logAppender.addExpectation(new MockLogAppender.SeenEventExpectation( + "change to platinum", + listener.getClass().getName(), + Level.INFO, + "Active license is now [PLATINUM]; Security is enabled" + )); + listener.licenseStateChanged(); + + logAppender.assertAllExpectationsMatched(); + } + +}