Add availability for features and make kerberos use it

Closes #30730

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2024-06-25 15:11:43 +02:00 committed by Marek Posolda
parent ca26524259
commit c20dbc5c32
2 changed files with 63 additions and 4 deletions

View file

@ -34,6 +34,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.function.BooleanSupplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -83,7 +84,7 @@ public class Profile {
STEP_UP_AUTHENTICATION("Step-up Authentication", Type.DEFAULT), STEP_UP_AUTHENTICATION("Step-up Authentication", Type.DEFAULT),
// Check if kerberos is available in underlying JVM and auto-detect if feature should be enabled or disabled by default based on that // Check if kerberos is available in underlying JVM and auto-detect if feature should be enabled or disabled by default based on that
KERBEROS("Kerberos", KerberosJdkProvider.getProvider().isKerberosAvailable() ? Type.DEFAULT : Type.DISABLED_BY_DEFAULT), KERBEROS("Kerberos", Type.DEFAULT, 1, () -> KerberosJdkProvider.getProvider().isKerberosAvailable()),
RECOVERY_CODES("Recovery codes", Type.PREVIEW), RECOVERY_CODES("Recovery codes", Type.PREVIEW),
@ -122,21 +123,27 @@ public class Profile {
private final String label; private final String label;
private final String unversionedKey; private final String unversionedKey;
private final String key; private final String key;
private final BooleanSupplier isAvailable;
private Set<Feature> dependencies; private Set<Feature> dependencies;
private int version; private int version;
Feature(String label, Type type, Feature... dependencies) { Feature(String label, Type type, Feature... dependencies) {
this(label, type, 1, dependencies); this(label, type, 1, null, dependencies);
}
Feature(String label, Type type, int version, Feature... dependencies) {
this(label, type, version, null, dependencies);
} }
/** /**
* allowNameKey should be false for new versioned features to disallow using a legacy name, like account2 * allowNameKey should be false for new versioned features to disallow using a legacy name, like account2
*/ */
Feature(String label, Type type, int version, Feature... dependencies) { Feature(String label, Type type, int version, BooleanSupplier isAvailable, Feature... dependencies) {
this.label = label; this.label = label;
this.type = type; this.type = type;
this.version = version; this.version = version;
this.isAvailable = isAvailable;
this.key = name().toLowerCase().replaceAll("_", "-"); this.key = name().toLowerCase().replaceAll("_", "-");
if (this.name().endsWith("_V" + version)) { if (this.name().endsWith("_V" + version)) {
unversionedKey = key.substring(0, key.length() - (String.valueOf(version).length() + 2)); unversionedKey = key.substring(0, key.length() - (String.valueOf(version).length() + 2));
@ -190,6 +197,10 @@ public class Profile {
return version; return version;
} }
public boolean isAvailable() {
return isAvailable == null || isAvailable.getAsBoolean();
}
public enum Type { public enum Type {
// in priority order // in priority order
DEFAULT("Default"), DEFAULT("Default"),
@ -238,6 +249,9 @@ public class Profile {
Feature enabledFeature = null; Feature enabledFeature = null;
if (unversionedConfig == FeatureConfig.ENABLED) { if (unversionedConfig == FeatureConfig.ENABLED) {
enabledFeature = entry.getValue().iterator().next(); enabledFeature = entry.getValue().iterator().next();
if (!enabledFeature.isAvailable()) {
throw new ProfileException(String.format("Feature %s cannot be enabled as it is not available.", unversionedFeature));
}
} else if (unversionedConfig == FeatureConfig.DISABLED && ESSENTIAL_FEATURES.contains(unversionedFeature)) { } else if (unversionedConfig == FeatureConfig.DISABLED && ESSENTIAL_FEATURES.contains(unversionedFeature)) {
throw new ProfileException(String.format("Feature %s cannot be disabled.", unversionedFeature)); throw new ProfileException(String.format("Feature %s cannot be disabled.", unversionedFeature));
} }
@ -259,13 +273,17 @@ public class Profile {
enabledFeature.getVersionedKey(), f.getVersionedKey())); enabledFeature.getVersionedKey(), f.getVersionedKey()));
} }
// even if something else was enabled by default, explicitly enabling a lower priority feature takes precedence // even if something else was enabled by default, explicitly enabling a lower priority feature takes precedence
if (!f.isAvailable()) {
throw new ProfileException(String.format("Feature %s cannot be enabled as it is not available.", f.getVersionedKey()));
}
enabledFeature = f; enabledFeature = f;
isExplicitlyEnabledFeature = true; isExplicitlyEnabledFeature = true;
break; break;
case DISABLED: case DISABLED:
throw new ProfileException("Feature " + f.getVersionedKey() + " should not be disabled using a versioned key."); throw new ProfileException("Feature " + f.getVersionedKey() + " should not be disabled using a versioned key.");
default: default:
if (unversionedConfig == FeatureConfig.UNCONFIGURED && enabledFeature == null && isEnabledByDefault(profile, f)) { if (unversionedConfig == FeatureConfig.UNCONFIGURED && enabledFeature == null
&& isEnabledByDefault(profile, f) && f.isAvailable()) {
enabledFeature = f; enabledFeature = f;
} }
break; break;

View file

@ -12,10 +12,14 @@ import org.keycloak.common.profile.CommaSeparatedListProfileConfigResolver;
import org.keycloak.common.profile.ProfileException; import org.keycloak.common.profile.ProfileException;
import org.keycloak.common.profile.PropertiesProfileConfigResolver; import org.keycloak.common.profile.PropertiesProfileConfigResolver;
import java.security.Provider;
import java.security.Security;
import java.util.AbstractMap;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
@ -228,6 +232,32 @@ public class ProfileTest {
Assert.assertTrue(Profile.isFeatureEnabled(PREVIEW_FEATURE)); Assert.assertTrue(Profile.isFeatureEnabled(PREVIEW_FEATURE));
} }
@Test
public void kerberosConfigAvailability() {
// remove SunJGSS to remove kerberos availability
Map.Entry<Integer, Provider> removed = removeSecurityProvider("SunJGSS");
try {
Properties properties = new Properties();
properties.setProperty(PropertiesProfileConfigResolver.getPropertyKey(Profile.Feature.KERBEROS), "enabled");
ProfileException e = Assert.assertThrows(ProfileException.class, () -> Profile.configure(new PropertiesProfileConfigResolver(properties)));
Assert.assertEquals("Feature kerberos cannot be enabled as it is not available.", e.getMessage());
Profile.defaults();
properties.setProperty(PropertiesProfileConfigResolver.getPropertyKey(Profile.Feature.KERBEROS), "disabled");
Profile.configure(new PropertiesProfileConfigResolver(properties));
Assert.assertFalse(Profile.isFeatureEnabled(Profile.Feature.KERBEROS));
Profile.defaults();
properties.clear();
Profile.configure(new PropertiesProfileConfigResolver(properties));
Assert.assertFalse(Profile.isFeatureEnabled(Profile.Feature.KERBEROS));
} finally {
if (removed != null) {
Security.insertProviderAt(removed.getValue(), removed.getKey());
}
}
}
public static void assertEquals(Set<Profile.Feature> actual, Collection<Profile.Feature> expected) { public static void assertEquals(Set<Profile.Feature> actual, Collection<Profile.Feature> expected) {
MatcherAssert.assertThat(actual, Matchers.equalTo(expected)); MatcherAssert.assertThat(actual, Matchers.equalTo(expected));
} }
@ -243,4 +273,15 @@ public class ProfileTest {
} }
} }
private Map.Entry<Integer, Provider> removeSecurityProvider(String name) {
int position = 1;
for (Provider p : Security.getProviders()) {
if (name.equals(p.getName())) {
Security.removeProvider(name);
return new AbstractMap.SimpleEntry<>(position, p);
}
position++;
}
return null;
}
} }