[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
This commit is contained in:
Tim Vernum 2020-10-07 17:40:24 +11:00 committed by GitHub
parent 8a61b95a0f
commit eeb45b4a74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 114 additions and 37 deletions

View File

@ -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'

View File

@ -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"));
}
}
}

View File

@ -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);

View File

@ -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

View File

@ -35,6 +35,9 @@ public class SpiExtensionPlugin extends Plugin implements ActionPlugin {
List<Setting<?>> 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;
}
}

View File

@ -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<String> 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<String> USERNAME_SETTING = RealmSettings.simpleString(TYPE, "username",
Setting.Property.NodeScope, Setting.Property.Filtered);
public static final Setting.AffixSetting<SecureString> 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<List<String>> 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<AuthenticationResult> 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<User> listener) {
// Lookup (run-as) is not supported in this realm
listener.onResponse(null);
}
}

View File

@ -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<AuthenticationResult> 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<AuthenticationResult> 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<AuthenticationResult> plainActionFuture = new PlainActionFuture<>();
realm.authenticate(token, plainActionFuture);
final AuthenticationResult result = plainActionFuture.actionGet();