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.Set;
import java.util.TreeSet;
import java.util.function.BooleanSupplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -83,7 +84,7 @@ public class Profile {
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
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),
@ -122,21 +123,27 @@ public class Profile {
private final String label;
private final String unversionedKey;
private final String key;
private final BooleanSupplier isAvailable;
private Set<Feature> dependencies;
private int version;
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
*/
Feature(String label, Type type, int version, Feature... dependencies) {
Feature(String label, Type type, int version, BooleanSupplier isAvailable, Feature... dependencies) {
this.label = label;
this.type = type;
this.version = version;
this.isAvailable = isAvailable;
this.key = name().toLowerCase().replaceAll("_", "-");
if (this.name().endsWith("_V" + version)) {
unversionedKey = key.substring(0, key.length() - (String.valueOf(version).length() + 2));
@ -190,6 +197,10 @@ public class Profile {
return version;
}
public boolean isAvailable() {
return isAvailable == null || isAvailable.getAsBoolean();
}
public enum Type {
// in priority order
DEFAULT("Default"),
@ -238,6 +249,9 @@ public class Profile {
Feature enabledFeature = null;
if (unversionedConfig == FeatureConfig.ENABLED) {
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)) {
throw new ProfileException(String.format("Feature %s cannot be disabled.", unversionedFeature));
}
@ -259,13 +273,17 @@ public class Profile {
enabledFeature.getVersionedKey(), f.getVersionedKey()));
}
// 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;
isExplicitlyEnabledFeature = true;
break;
case DISABLED:
throw new ProfileException("Feature " + f.getVersionedKey() + " should not be disabled using a versioned key.");
default:
if (unversionedConfig == FeatureConfig.UNCONFIGURED && enabledFeature == null && isEnabledByDefault(profile, f)) {
if (unversionedConfig == FeatureConfig.UNCONFIGURED && enabledFeature == null
&& isEnabledByDefault(profile, f) && f.isAvailable()) {
enabledFeature = f;
}
break;

View file

@ -12,10 +12,14 @@ import org.keycloak.common.profile.CommaSeparatedListProfileConfigResolver;
import org.keycloak.common.profile.ProfileException;
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.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
@ -228,6 +232,32 @@ public class ProfileTest {
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) {
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;
}
}