Skip to content

Commit

Permalink
Support excluding roles for SAML realm (elastic#105445)
Browse files Browse the repository at this point in the history
This PR adds a new `exclude_roles` setting  for SAML realm.  
This setting allows to exclude certain roles from being mapped 
to users that are authenticated via SAML realm - regardless of 
the configured role mappings.

The `exclude_roles` setting supports only explicit role names.
Regular expressions and wildcards are not supported.

The exclusion is possible only if the role mapping is handled 
by the SAML realm. Hence, it is not possible to configure it 
along with `authorization_realms` setting.

Note: It is intentional that this setting is not registered in this PR. 
The registration will be addressed in a separate PR.
  • Loading branch information
slobodanadamovic authored Feb 14, 2024
1 parent a874f47 commit 4f07020
Show file tree
Hide file tree
Showing 8 changed files with 403 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.elasticsearch.xpack.core.security.authc.support.SecuritySettingsUtil.verifyNonNullNotEmpty;

/**
* Settings unique to each JWT realm.
*/
Expand Down Expand Up @@ -491,34 +493,6 @@ public Iterator<Setting<?>> settings() {
public static final Collection<Setting.AffixSetting<?>> DELEGATED_AUTHORIZATION_REALMS_SETTINGS = DelegatedAuthorizationSettings
.getSettings(TYPE);

private static void verifyNonNullNotEmpty(final String key, final String value, final List<String> allowedValues) {
assert value != null : "Invalid null value for [" + key + "].";
if (value.isEmpty()) {
throw new IllegalArgumentException("Invalid empty value for [" + key + "].");
}
if (allowedValues != null) {
if (allowedValues.contains(value) == false) {
throw new IllegalArgumentException(
"Invalid value [" + value + "] for [" + key + "]. Allowed values are " + allowedValues + "."
);
}
}
}

private static void verifyNonNullNotEmpty(final String key, final List<String> values, final List<String> allowedValues) {
assert values != null : "Invalid null list of values for [" + key + "].";
if (values.isEmpty()) {
if (allowedValues == null) {
throw new IllegalArgumentException("Invalid empty list for [" + key + "].");
} else {
throw new IllegalArgumentException("Invalid empty list for [" + key + "]. Allowed values are " + allowedValues + ".");
}
}
for (final String value : values) {
verifyNonNullNotEmpty(key, value, allowedValues);
}
}

private static void validateFallbackClaimSetting(
Setting.AffixSetting<String> setting,
String key,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package org.elasticsearch.xpack.core.security.authc.saml;

import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.SettingsException;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
Expand All @@ -18,9 +19,13 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.elasticsearch.xpack.core.security.authc.support.SecuritySettingsUtil.verifyNonNullNotEmpty;

public class SamlRealmSettings {

public static final String TYPE = "saml";
Expand Down Expand Up @@ -140,6 +145,47 @@ public class SamlRealmSettings {
key -> Setting.positiveTimeSetting(key, TimeValue.timeValueMinutes(3), Setting.Property.NodeScope)
);

public static final Setting.AffixSetting<List<String>> EXCLUDE_ROLES = Setting.affixKeySetting(
RealmSettings.realmSettingPrefix(TYPE),
"exclude_roles",
key -> Setting.stringListSetting(key, new Setting.Validator<>() {

@Override
public void validate(List<String> excludedRoles) {
excludedRoles.forEach(excludedRole -> verifyNonNullNotEmpty(key, excludedRole));
}

@Override
public void validate(List<String> excludedRoles, Map<Setting<?>, Object> settings) {
if (false == excludedRoles.isEmpty()) {
final String namespace = EXCLUDE_ROLES.getNamespace(EXCLUDE_ROLES.getConcreteSetting(key));
final Setting<List<String>> authorizationRealmsSetting = DelegatedAuthorizationSettings.AUTHZ_REALMS.apply(TYPE)
.getConcreteSettingForNamespace(namespace);
@SuppressWarnings("unchecked")
final List<String> authorizationRealms = (List<String>) settings.get(authorizationRealmsSetting);
if (authorizationRealms != null && false == authorizationRealms.isEmpty()) {
throw new SettingsException(
"Setting ["
+ EXCLUDE_ROLES.getConcreteSettingForNamespace(namespace).getKey()
+ "] is not permitted when setting ["
+ authorizationRealmsSetting.getKey()
+ "] is configured."
);
}
}
}

@Override
public Iterator<Setting<?>> settings() {
final String namespace = EXCLUDE_ROLES.getNamespace(EXCLUDE_ROLES.getConcreteSetting(key));
final List<Setting<?>> settings = List.of(
DelegatedAuthorizationSettings.AUTHZ_REALMS.apply(TYPE).getConcreteSettingForNamespace(namespace)
);
return settings.iterator();
}
}, Setting.Property.NodeScope)
);

public static final String SSL_PREFIX = "ssl.";

private SamlRealmSettings() {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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.core.security.authc.support;

import java.util.Collection;
import java.util.List;

/**
* Utilities for validating security settings.
*/
public final class SecuritySettingsUtil {

/**
* Validates that a given setting's value is not empty nor null.
*
* @param settingKey The full setting key which is validated. Used for building a proper error messages.
* @param settingValue The value to validate that it's not null nor empty.
*/
public static void verifyNonNullNotEmpty(final String settingKey, final String settingValue) {
verifyNonNullNotEmpty(settingKey, settingValue, null);
}

/**
* Validates that a given setting's value is not empty nor null and that it is one of the allowed values.
*
* @param settingKey The full setting key which is validated. Used for building a proper error messages.
* @param settingValue The value to validate that it's not null nor empty and that is one of the allowed values.
* @param allowedValues Optional allowed values, against which to validate the given setting value.
* If provided, it will be checked that the setting value is one of these allowed values.
*/
public static void verifyNonNullNotEmpty(final String settingKey, final String settingValue, final Collection<String> allowedValues) {
assert settingValue != null : "Invalid null value for [" + settingKey + "].";
if (settingValue.isEmpty()) {
throw new IllegalArgumentException("Invalid empty value for [" + settingKey + "].");
}
if (allowedValues != null) {
if (allowedValues.contains(settingValue) == false) {
throw new IllegalArgumentException(
"Invalid value [" + settingValue + "] for [" + settingKey + "]. Allowed values are " + allowedValues + "."
);
}
}
}

/**
* Validates that a given setting's values are not empty nor null.
*
* @param settingKey The full setting key which is validated. Used for building a proper error messages.
* @param settingValues The values to validate that are not null nor empty.
*/
public static void verifyNonNullNotEmpty(final String settingKey, final List<String> settingValues) {
verifyNonNullNotEmpty(settingKey, settingValues, null);
}

/**
* Validates that a given setting's values are not empty nor null and that are one of the allowed values.
*
* @param settingKey The full setting key which is validated. Used for building a proper error messages.
* @param settingValues The values to validate that are not null nor empty and that are one of the allowed values.
* @param allowedValues The allowed values against which to validate the given setting values.
* If provided, this method will check that the setting values are one of these allowed values.
*/
public static void verifyNonNullNotEmpty(
final String settingKey,
final List<String> settingValues,
final Collection<String> allowedValues
) {
assert settingValues != null : "Invalid null list of values for [" + settingKey + "].";
if (settingValues.isEmpty()) {
if (allowedValues == null) {
throw new IllegalArgumentException("Invalid empty list for [" + settingKey + "].");
} else {
throw new IllegalArgumentException(
"Invalid empty list for [" + settingKey + "]. Allowed values are " + allowedValues + "."
);
}
}
for (final String settingValue : settingValues) {
verifyNonNullNotEmpty(settingKey, settingValue, allowedValues);
}
}

private SecuritySettingsUtil() {
throw new IllegalAccessError("not allowed!");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ public void testIncludeReservedRolesSettingNotRegistered() {
.putList("xpack.security.reserved_roles.include", "superuser");

final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> internalCluster().startNode(builder));
assertThat(e.getMessage(), containsString("unknown setting"));
assertThat(e.getMessage(), containsString("unknown setting [xpack.security.reserved_roles.include]"));
}

public void testSamlExcludeRolesSettingNotRegistered() throws Exception {
internalCluster().setBootstrapMasterNodeIndex(0);

Settings.Builder builder = Settings.builder()
.put(randomBoolean() ? masterNode() : dataOnlyNode())
.putList("xpack.security.authc.realms.saml.saml1.exclude_roles", "superuser");

final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> internalCluster().startNode(builder));
assertThat(e.getMessage(), containsString("unknown setting [xpack.security.authc.realms.saml.saml1.exclude_roles]"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import org.elasticsearch.xpack.security.authc.Realms;
import org.elasticsearch.xpack.security.authc.TokenService;
import org.elasticsearch.xpack.security.authc.support.DelegatedAuthorizationSupport;
import org.elasticsearch.xpack.security.authc.support.mapper.ExcludingRoleMapper;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.criterion.EntityRoleCriterion;
Expand Down Expand Up @@ -261,7 +262,11 @@ public SpConfiguration getServiceProvider() {
) throws Exception {
super(config);

this.roleMapper = roleMapper;
if (config.hasSetting(SamlRealmSettings.EXCLUDE_ROLES)) {
this.roleMapper = new ExcludingRoleMapper(roleMapper, config.getSetting(SamlRealmSettings.EXCLUDE_ROLES));
} else {
this.roleMapper = roleMapper;
}
this.authenticator = authenticator;
this.logoutHandler = logoutHandler;
this.logoutResponseHandler = logoutResponseHandler;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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.authc.support.mapper;

import org.elasticsearch.action.ActionListener;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.xpack.core.security.authc.support.CachingRealm;
import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper;

import java.util.Collection;
import java.util.Objects;
import java.util.Set;

/**
* Implementation of role mapper which wraps a {@link UserRoleMapper}
* and filters out the resolved roles by removing the configured roles to exclude.
*/
public class ExcludingRoleMapper implements UserRoleMapper {

private final UserRoleMapper delegate;
private final Set<String> rolesToExclude;

public ExcludingRoleMapper(UserRoleMapper delegate, Collection<String> rolesToExclude) {
this.delegate = Objects.requireNonNull(delegate);
this.rolesToExclude = Set.copyOf(rolesToExclude);
}

@Override
public void resolveRoles(UserData user, ActionListener<Set<String>> listener) {
delegate.resolveRoles(user, listener.delegateFailureAndWrap((l, r) -> l.onResponse(excludeRoles(r))));
}

private Set<String> excludeRoles(Set<String> resolvedRoles) {
if (rolesToExclude.isEmpty()) {
return resolvedRoles;
} else {
return Sets.difference(resolvedRoles, rolesToExclude);
}
}

@Override
public void refreshRealmOnChange(CachingRealm realm) {
delegate.refreshRealmOnChange(realm);
}
}
Loading

0 comments on commit 4f07020

Please sign in to comment.