From eeb45b4a74ed5a2b4cf9901df0f9860abe939f86 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Wed, 7 Oct 2020 17:40:24 +1100 Subject: [PATCH] [Backport 7.x] Add example settings to sample security realm (#63301) This change adds configurable settings to the `CustomRealm` in the QA project as the correct declaration and use of settings can be a source of confusion in custom realms. The "username" "password" and "roles" are now all configurable, which demonstrates the use of a simple string setting ("username") a secure setting ("password") and a more complex list setting ("roles"). Backport of: #62287 --- .../build.gradle | 7 ++- .../example/realm/CustomRealmIT.java | 31 +++++++----- .../realm/CustomRoleMappingRealmIT.java | 8 ++-- .../example/role/CustomRolesProviderIT.java | 9 ++-- .../example/SpiExtensionPlugin.java | 3 ++ .../example/realm/CustomRealm.java | 47 ++++++++++++++++--- .../example/realm/CustomRealmTests.java | 46 +++++++++++++++--- 7 files changed, 114 insertions(+), 37 deletions(-) diff --git a/x-pack/qa/security-example-spi-extension/build.gradle b/x-pack/qa/security-example-spi-extension/build.gradle index 3546276dc04..c6a4cbd882f 100644 --- a/x-pack/qa/security-example-spi-extension/build.gradle +++ b/x-pack/qa/security-example-spi-extension/build.gradle @@ -29,8 +29,11 @@ testClusters.all { // processors are being used that are in ingest-common module. testDistribution = 'DEFAULT' - setting 'xpack.security.authc.realms.custom.custom.order', '0' - setting 'xpack.security.authc.realms.custom.custom.filtered_setting', 'should be filtered' + setting 'xpack.security.authc.realms.custom.my_realm.order', '0' + setting 'xpack.security.authc.realms.custom.my_realm.filtered_setting', 'should be filtered' + setting 'xpack.security.authc.realms.custom.my_realm.username', 'test_user' + keystore 'xpack.security.authc.realms.custom.my_realm.password', 'secret_password' + setting 'xpack.security.authc.realms.file.esusers.order', '1' setting 'xpack.security.authc.realms.native.native.order', '2' setting 'xpack.security.authc.realms.custom_role_mapping.role_map.order', '3' diff --git a/x-pack/qa/security-example-spi-extension/src/javaRestTest/java/org/elasticsearch/example/realm/CustomRealmIT.java b/x-pack/qa/security-example-spi-extension/src/javaRestTest/java/org/elasticsearch/example/realm/CustomRealmIT.java index f33ecf7144d..1aa73baf89d 100644 --- a/x-pack/qa/security-example-spi-extension/src/javaRestTest/java/org/elasticsearch/example/realm/CustomRealmIT.java +++ b/x-pack/qa/security-example-spi-extension/src/javaRestTest/java/org/elasticsearch/example/realm/CustomRealmIT.java @@ -37,13 +37,17 @@ import static org.hamcrest.Matchers.is; */ public class CustomRealmIT extends ESIntegTestCase { + // These are configured in build.gradle + public static final String USERNAME= "test_user"; + public static final String PASSWORD = "secret_password"; + @Override protected Settings externalClusterClientSettings() { return Settings.builder() - .put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, CustomRealm.KNOWN_USER) - .put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, CustomRealm.KNOWN_PW.toString()) - .put(NetworkModule.TRANSPORT_TYPE_KEY, "security4") - .build(); + .put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, USERNAME) + .put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, PASSWORD) + .put(NetworkModule.TRANSPORT_TYPE_KEY, "security4") + .build(); } @Override @@ -66,8 +70,8 @@ public class CustomRealmIT extends ESIntegTestCase { public void testHttpAuthentication() throws Exception { Request request = new Request("GET", "/"); RequestOptions.Builder options = request.getOptions().toBuilder(); - options.addHeader(CustomRealm.USER_HEADER, CustomRealm.KNOWN_USER); - options.addHeader(CustomRealm.PW_HEADER, CustomRealm.KNOWN_PW.toString()); + options.addHeader(CustomRealm.USER_HEADER, USERNAME); + options.addHeader(CustomRealm.PW_HEADER, PASSWORD); request.setOptions(options); Response response = getRestClient().performRequest(request); assertThat(response.getStatusLine().getStatusCode(), is(200)); @@ -83,8 +87,8 @@ public class CustomRealmIT extends ESIntegTestCase { Settings settings = Settings.builder() .put("cluster.name", clusterName) .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toAbsolutePath().toString()) - .put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, CustomRealm.KNOWN_USER) - .put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, CustomRealm.KNOWN_PW.toString()) + .put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, USERNAME) + .put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, PASSWORD) .build(); try (TransportClient client = new PreBuiltXPackTransportClient(settings)) { client.addTransportAddress(publishAddress); @@ -103,8 +107,8 @@ public class CustomRealmIT extends ESIntegTestCase { Settings settings = Settings.builder() .put("cluster.name", clusterName) .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toAbsolutePath().toString()) - .put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, CustomRealm.KNOWN_USER + randomAlphaOfLength(1)) - .put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, CustomRealm.KNOWN_PW.toString()) + .put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, USERNAME + randomAlphaOfLength(1)) + .put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, PASSWORD) .build(); try (TransportClient client = new PreBuiltXPackTransportClient(settings)) { client.addTransportAddress(publishAddress); @@ -116,12 +120,13 @@ public class CustomRealmIT extends ESIntegTestCase { } public void testSettingsFiltering() throws Exception { - NodesInfoResponse nodeInfos = client().admin().cluster().prepareNodesInfo().clear().addMetric(Metric.SETTINGS.metricName()).get(); + NodesInfoResponse nodeInfos = client().admin() + .cluster().prepareNodesInfo().clear().addMetric(Metric.SETTINGS.metricName()).get(); for(NodeInfo info : nodeInfos.getNodes()) { Settings settings = info.getSettings(); assertNotNull(settings); - assertNull(settings.get("xpack.security.authc.realms.custom.custom.filtered_setting")); - assertEquals("0", settings.get("xpack.security.authc.realms.custom.custom.order")); + assertNull(settings.get("xpack.security.authc.realms.custom.my_realm.filtered_setting")); + assertEquals("0", settings.get("xpack.security.authc.realms.custom.my_realm.order")); } } } diff --git a/x-pack/qa/security-example-spi-extension/src/javaRestTest/java/org/elasticsearch/example/realm/CustomRoleMappingRealmIT.java b/x-pack/qa/security-example-spi-extension/src/javaRestTest/java/org/elasticsearch/example/realm/CustomRoleMappingRealmIT.java index 02f2bdc0d0d..947134195fc 100644 --- a/x-pack/qa/security-example-spi-extension/src/javaRestTest/java/org/elasticsearch/example/realm/CustomRoleMappingRealmIT.java +++ b/x-pack/qa/security-example-spi-extension/src/javaRestTest/java/org/elasticsearch/example/realm/CustomRoleMappingRealmIT.java @@ -31,8 +31,8 @@ public class CustomRoleMappingRealmIT extends ESRestTestCase { @Override protected Settings restClientSettings() { return Settings.builder() - .put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, CustomRealm.KNOWN_USER) - .put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, CustomRealm.KNOWN_PW.toString()) + .put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, CustomRealmIT.USERNAME) + .put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, CustomRealmIT.PASSWORD) .build(); } @@ -54,8 +54,8 @@ public class CustomRoleMappingRealmIT extends ESRestTestCase { Request request = new Request("GET", "/_security/_authenticate"); RequestOptions.Builder options = request.getOptions().toBuilder(); // Authenticate as the custom realm superuser - options.addHeader(CustomRealm.USER_HEADER, CustomRealm.KNOWN_USER); - options.addHeader(CustomRealm.PW_HEADER, CustomRealm.KNOWN_PW.toString()); + options.addHeader(CustomRealm.USER_HEADER, CustomRealmIT.USERNAME); + options.addHeader(CustomRealm.PW_HEADER, CustomRealmIT.PASSWORD); // But "run-as" the role mapped user options.addHeader("es-security-runas-user", CustomRoleMappingRealm.USERNAME); request.setOptions(options); diff --git a/x-pack/qa/security-example-spi-extension/src/javaRestTest/java/org/elasticsearch/example/role/CustomRolesProviderIT.java b/x-pack/qa/security-example-spi-extension/src/javaRestTest/java/org/elasticsearch/example/role/CustomRolesProviderIT.java index c203ff24236..fbe003e3060 100644 --- a/x-pack/qa/security-example-spi-extension/src/javaRestTest/java/org/elasticsearch/example/role/CustomRolesProviderIT.java +++ b/x-pack/qa/security-example-spi-extension/src/javaRestTest/java/org/elasticsearch/example/role/CustomRolesProviderIT.java @@ -18,6 +18,7 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.xpack.core.XPackClientPlugin; import org.elasticsearch.xpack.core.security.authc.support.Hasher; +import org.elasticsearch.example.realm.CustomRealmIT; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.client.SecurityClient; @@ -47,10 +48,10 @@ public class CustomRolesProviderIT extends ESIntegTestCase { @Override protected Settings externalClusterClientSettings() { return Settings.builder() - .put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, CustomRealm.KNOWN_USER) - .put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, CustomRealm.KNOWN_PW.toString()) - .put(NetworkModule.TRANSPORT_TYPE_KEY, "security4") - .build(); + .put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, CustomRealmIT.USERNAME) + .put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, CustomRealmIT.PASSWORD) + .put(NetworkModule.TRANSPORT_TYPE_KEY, "security4") + .build(); } @Override diff --git a/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/SpiExtensionPlugin.java b/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/SpiExtensionPlugin.java index ee47fa0bf7b..03a7cd560fc 100644 --- a/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/SpiExtensionPlugin.java +++ b/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/SpiExtensionPlugin.java @@ -35,6 +35,9 @@ public class SpiExtensionPlugin extends Plugin implements ActionPlugin { List> list = new ArrayList<>(RealmSettings.getStandardSettings(CustomRealm.TYPE)); list.add(RealmSettings.simpleString(CustomRealm.TYPE, "filtered_setting", Setting.Property.NodeScope, Setting.Property.Filtered)); list.addAll(RealmSettings.getStandardSettings(CustomRoleMappingRealm.TYPE)); + list.add(CustomRealm.USERNAME_SETTING); + list.add(CustomRealm.PASSWORD_SETTING); + list.add(CustomRealm.ROLES_SETTING); return list; } } diff --git a/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/realm/CustomRealm.java b/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/realm/CustomRealm.java index b62eb4cae0e..54c67cc66c4 100644 --- a/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/realm/CustomRealm.java +++ b/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/realm/CustomRealm.java @@ -6,16 +6,23 @@ package org.elasticsearch.example.realm; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.CharArrays; import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.example.SpiExtensionPlugin; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.RealmConfig; -import org.elasticsearch.common.CharArrays; +import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.user.User; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + public class CustomRealm extends Realm { public static final String TYPE = "custom"; @@ -23,12 +30,37 @@ public class CustomRealm extends Realm { public static final String USER_HEADER = "User"; public static final String PW_HEADER = "Password"; - public static final String KNOWN_USER = "custom_user"; - public static final SecureString KNOWN_PW = new SecureString("x-pack-test-password".toCharArray()); - static final String[] ROLES = new String[] { "superuser" }; + public static final String DEFAULT_KNOWN_USER = "custom_user"; + public static final SecureString DEFAULT_KNOWN_PW = new SecureString("x-pack-test-password".toCharArray()); + static final List DEFAULT_ROLES = Collections.singletonList("superuser"); + + // Because simple string settings in realms are common, this is a shorthand method, but it does the same thing as the ROLES_SETTING + // that is declared below (with the minor difference that "username" is a single string, and "roles" is a list) + public static final Setting.AffixSetting USERNAME_SETTING = RealmSettings.simpleString(TYPE, "username", + Setting.Property.NodeScope, Setting.Property.Filtered); + + public static final Setting.AffixSetting PASSWORD_SETTING = RealmSettings.secureString(TYPE, "password"); + + /** + * The setting is declared as an AffixSetting, because part of the setting name is variable (the name of the realm). + * An AffixSetting uses a factory method to construct a "concrete setting", which in this case is a list. + * It will be entered in elasticsearch.yml as "xpack.security.authc.realms.{TYPE}.{NAME}.roles" + * For example: {@code xpack.security.authc.realms.custom.your_realm_name.roles: [ "role1" , "role2" ]} + * @see SpiExtensionPlugin#getSettings() + */ + public static final Setting.AffixSetting> ROLES_SETTING = Setting.affixKeySetting( + RealmSettings.realmSettingPrefix(TYPE), + "roles", key -> Setting.listSetting(key, DEFAULT_ROLES, Function.identity(), Setting.Property.NodeScope)); + + private final String username; + private final SecureString password; + private final String[] roles; public CustomRealm(RealmConfig config) { super(config); + this.username = config.getSetting(USERNAME_SETTING, () -> DEFAULT_KNOWN_USER); + this.password = config.getSetting(PASSWORD_SETTING, () -> DEFAULT_KNOWN_PW); + this.roles = config.getSetting(ROLES_SETTING).toArray(new String[0]); } @Override @@ -52,9 +84,9 @@ public class CustomRealm extends Realm { public void authenticate(AuthenticationToken authToken, ActionListener listener) { UsernamePasswordToken token = (UsernamePasswordToken)authToken; final String actualUser = token.principal(); - if (KNOWN_USER.equals(actualUser)) { - if (CharArrays.constantTimeEquals(token.credentials().getChars(), KNOWN_PW.getChars())) { - listener.onResponse(AuthenticationResult.success(new User(actualUser, ROLES))); + if (username.equals(actualUser)) { + if (CharArrays.constantTimeEquals(token.credentials().getChars(), password.getChars())) { + listener.onResponse(AuthenticationResult.success(new User(actualUser, roles))); } else { listener.onResponse(AuthenticationResult.unsuccessful("Invalid password for user " + actualUser, null)); } @@ -65,6 +97,7 @@ public class CustomRealm extends Realm { @Override public void lookupUser(String username, ActionListener listener) { + // Lookup (run-as) is not supported in this realm listener.onResponse(null); } } diff --git a/x-pack/qa/security-example-spi-extension/src/test/java/org/elasticsearch/example/realm/CustomRealmTests.java b/x-pack/qa/security-example-spi-extension/src/test/java/org/elasticsearch/example/realm/CustomRealmTests.java index cc7579df27f..59cf127b029 100644 --- a/x-pack/qa/security-example-spi-extension/src/test/java/org/elasticsearch/example/realm/CustomRealmTests.java +++ b/x-pack/qa/security-example-spi-extension/src/test/java/org/elasticsearch/example/realm/CustomRealmTests.java @@ -6,6 +6,7 @@ package org.elasticsearch.example.realm; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -13,33 +14,64 @@ import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.user.User; +import static org.elasticsearch.xpack.core.security.authc.RealmSettings.getFullSettingKey; +import static org.hamcrest.Matchers.arrayContaining; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; public class CustomRealmTests extends ESTestCase { - public void testAuthenticate() { + + public void testAuthenticateDefaultConfig() { Settings globalSettings = Settings.builder().put("path.home", createTempDir()).build(); CustomRealm realm = new CustomRealm(new RealmConfig(new RealmConfig.RealmIdentifier(CustomRealm.TYPE, "test"), globalSettings, TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings))); - SecureString password = CustomRealm.KNOWN_PW.clone(); - UsernamePasswordToken token = new UsernamePasswordToken(CustomRealm.KNOWN_USER, password); + SecureString password = CustomRealm.DEFAULT_KNOWN_PW.clone(); + UsernamePasswordToken token = new UsernamePasswordToken(CustomRealm.DEFAULT_KNOWN_USER, password); PlainActionFuture plainActionFuture = new PlainActionFuture<>(); realm.authenticate(token, plainActionFuture); User user = plainActionFuture.actionGet().getUser(); assertThat(user, notNullValue()); - assertThat(user.roles(), equalTo(CustomRealm.ROLES)); - assertThat(user.principal(), equalTo(CustomRealm.KNOWN_USER)); + assertThat(user.roles(), arrayContaining(CustomRealm.DEFAULT_ROLES.toArray())); + assertThat(user.principal(), equalTo(CustomRealm.DEFAULT_KNOWN_USER)); + } + + public void testAuthenticateCustomConfig() { + final RealmConfig.RealmIdentifier realmIdentifier = new RealmConfig.RealmIdentifier(CustomRealm.TYPE, "test"); + MockSecureSettings secureSettings = new MockSecureSettings(); + final String password = randomAlphaOfLengthBetween(8, 24); + secureSettings.setString(getFullSettingKey(realmIdentifier.getName(), CustomRealm.PASSWORD_SETTING), password); + Settings settings = Settings.builder() + .put("path.home", createTempDir()) + .put(getFullSettingKey(realmIdentifier, RealmSettings.ORDER_SETTING), 0) + .put(getFullSettingKey(realmIdentifier.getName(), CustomRealm.USERNAME_SETTING), "skroob") + .setSecureSettings(secureSettings) + .putList(getFullSettingKey(realmIdentifier.getName(), CustomRealm.ROLES_SETTING), "president", "villain") + .build(); + CustomRealm realm = new CustomRealm(new RealmConfig( + realmIdentifier, + settings, + TestEnvironment.newEnvironment(settings), + new ThreadContext(settings) + )); + UsernamePasswordToken token = new UsernamePasswordToken("skroob", new SecureString(password.toCharArray())); + PlainActionFuture plainActionFuture = new PlainActionFuture<>(); + realm.authenticate(token, plainActionFuture); + User user = plainActionFuture.actionGet().getUser(); + assertThat(user, notNullValue()); + assertThat(user.roles(), arrayContaining("president", "villain")); + assertThat(user.principal(), equalTo("skroob")); } public void testAuthenticateBadUser() { Settings globalSettings = Settings.builder().put("path.home", createTempDir()).build(); CustomRealm realm = new CustomRealm(new RealmConfig(new RealmConfig.RealmIdentifier(CustomRealm.TYPE, "test"), globalSettings, TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings))); - SecureString password = CustomRealm.KNOWN_PW.clone(); - UsernamePasswordToken token = new UsernamePasswordToken(CustomRealm.KNOWN_USER + "1", password); + SecureString password = CustomRealm.DEFAULT_KNOWN_PW.clone(); + UsernamePasswordToken token = new UsernamePasswordToken(CustomRealm.DEFAULT_KNOWN_USER + "1", password); PlainActionFuture plainActionFuture = new PlainActionFuture<>(); realm.authenticate(token, plainActionFuture); final AuthenticationResult result = plainActionFuture.actionGet();