All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.keycloak.common.Profile Maven / Gradle / Ivy

There is a newer version: 26.0.2
Show newest version
/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.keycloak.common;

import org.jboss.logging.Logger;
import org.keycloak.common.Profile.Feature.Type;
import org.keycloak.common.profile.ProfileConfigResolver;
import org.keycloak.common.profile.ProfileConfigResolver.FeatureConfig;
import org.keycloak.common.profile.ProfileException;
import org.keycloak.common.util.KerberosJdkProvider;

import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
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;

/**
 * @author Bill Burke
 * @version $Revision: 1 $
 */
public class Profile {

    private static volatile Map> FEATURES;

    public enum Feature {
        AUTHORIZATION("Authorization Service", Type.DEFAULT),

        ACCOUNT_API("Account Management REST API", Type.DEFAULT),

        ACCOUNT3("Account Console version 3", Type.DEFAULT, Feature.ACCOUNT_API),

        ADMIN_FINE_GRAINED_AUTHZ("Fine-Grained Admin Permissions", Type.PREVIEW),

        ADMIN_API("Admin API", Type.DEFAULT),

        ADMIN2("New Admin Console", Type.DEFAULT, Feature.ADMIN_API),

        LOGIN2("New Login Theme", Type.DEFAULT),

        LOGIN1("Legacy Login Theme", Type.DEPRECATED),

        DOCKER("Docker Registry protocol", Type.DISABLED_BY_DEFAULT),

        IMPERSONATION("Ability for admins to impersonate users", Type.DEFAULT),

        SCRIPTS("Write custom authenticators using JavaScript", Type.PREVIEW),

        TOKEN_EXCHANGE("Token Exchange Service", Type.PREVIEW),

        WEB_AUTHN("W3C Web Authentication (WebAuthn)", Type.DEFAULT),

        CLIENT_POLICIES("Client configuration policies", Type.DEFAULT),

        CIBA("OpenID Connect Client Initiated Backchannel Authentication (CIBA)", Type.DEFAULT),

        PAR("OAuth 2.0 Pushed Authorization Requests (PAR)", Type.DEFAULT),

        DYNAMIC_SCOPES("Dynamic OAuth 2.0 scopes", Type.EXPERIMENTAL),

        CLIENT_SECRET_ROTATION("Client Secret Rotation", Type.PREVIEW),

        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", Type.DEFAULT, 1, () -> KerberosJdkProvider.getProvider().isKerberosAvailable()),

        RECOVERY_CODES("Recovery codes", Type.PREVIEW),

        UPDATE_EMAIL("Update Email Action", Type.PREVIEW),

        JS_ADAPTER("Host keycloak.js and keycloak-authz.js through the Keycloak server", Type.DEFAULT),

        FIPS("FIPS 140-2 mode", Type.DISABLED_BY_DEFAULT),

        DPOP("OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer", Type.PREVIEW),

        DEVICE_FLOW("OAuth 2.0 Device Authorization Grant", Type.DEFAULT),

        TRANSIENT_USERS("Transient users for brokering", Type.EXPERIMENTAL),

        MULTI_SITE("Multi-site support", Type.DISABLED_BY_DEFAULT),

        REMOTE_CACHE("Remote caches support. Requires Multi-site support to be enabled as well.", Type.EXPERIMENTAL),

        CLIENT_TYPES("Client Types", Type.EXPERIMENTAL),

        HOSTNAME_V1("Hostname Options V1", Type.DEPRECATED, 1),
        HOSTNAME_V2("Hostname Options V2", Type.DEFAULT, 2),

        PERSISTENT_USER_SESSIONS("Persistent online user sessions across restarts and upgrades", Type.PREVIEW),

        OID4VC_VCI("Support for the OID4VCI protocol as part of OID4VC.", Type.EXPERIMENTAL),

        DECLARATIVE_UI("declarative ui spi", Type.EXPERIMENTAL),

        ORGANIZATION("Organization support within realms", Type.PREVIEW),

        PASSKEYS("Passkeys", Type.PREVIEW)
        ;

        private final Type type;
        private final String label;
        private final String unversionedKey;
        private final String key;
        private final BooleanSupplier isAvailable;

        private Set dependencies;
        private int version;

        Feature(String label, Type type, Feature... 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, 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));
            } else {
                this.unversionedKey = key;
                if (this.version > 1) {
                    throw new IllegalStateException("It is expected that the enum name ends with the version");
                }
            }
            this.dependencies = Arrays.stream(dependencies).collect(Collectors.toSet());
        }

        /**
         * Get the key that uniquely identifies this feature, may be used by users if
         * allowNameKey is true.
         * 

* {@link #getVersionedKey()} should instead be shown to users where possible. */ public String getKey() { return key; } /** * Return the key without any versioning. All features of the same type * will share this key. */ public String getUnversionedKey() { return unversionedKey; } /** * Return the key in the form key:v{version} */ public String getVersionedKey() { return getUnversionedKey() + ":v" + version; } public String getLabel() { return label; } public Type getType() { return type; } public Set getDependencies() { return dependencies; } public int getVersion() { return version; } public boolean isAvailable() { return isAvailable == null || isAvailable.getAsBoolean(); } public enum Type { // in priority order DEFAULT("Default"), DISABLED_BY_DEFAULT("Disabled by default"), DEPRECATED("Deprecated"), PREVIEW("Preview"), PREVIEW_DISABLED_BY_DEFAULT("Preview disabled by default"), // Preview features, which are not automatically enabled even with enabled preview profile (Needs to be enabled explicitly) EXPERIMENTAL("Experimental"); private final String label; Type(String label) { this.label = label; } public String getLabel() { return label; } } } private static final Set ESSENTIAL_FEATURES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(Feature.HOSTNAME_V2.getUnversionedKey()))); private static final Logger logger = Logger.getLogger(Profile.class); private static volatile Profile CURRENT; private final ProfileName profileName; private final Map features; public static Profile defaults() { return configure(); } public static Profile configure(ProfileConfigResolver... resolvers) { ProfileName profile = Arrays.stream(resolvers).map(ProfileConfigResolver::getProfileName).filter(Objects::nonNull).findFirst().orElse(ProfileName.DEFAULT); Map features = new LinkedHashMap<>(); for (Map.Entry> entry : getOrderedFeatures().entrySet()) { // first check by unversioned key - if enabled, choose the highest priority feature String unversionedFeature = entry.getKey(); ProfileConfigResolver.FeatureConfig unversionedConfig = getFeatureConfig(unversionedFeature, resolvers); 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)); } // now check each feature version to ensure consistency and select any features enabled by default boolean isExplicitlyEnabledFeature = false; for (Feature f : entry.getValue()) { ProfileConfigResolver.FeatureConfig configuration = getFeatureConfig(f.getVersionedKey(), resolvers); if (configuration != FeatureConfig.UNCONFIGURED && unversionedConfig != FeatureConfig.UNCONFIGURED) { throw new ProfileException("Versioned feature " + f.getVersionedKey() + " is not expected as " + unversionedFeature + " is already " + unversionedConfig.name().toLowerCase()); } switch (configuration) { case ENABLED: if (isExplicitlyEnabledFeature) { throw new ProfileException( String.format("Multiple versions of the same feature %s, %s should not be enabled.", 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) && f.isAvailable()) { enabledFeature = f; } break; } } for (Feature f : entry.getValue()) { features.put(f, f == enabledFeature); } } verifyConfig(features); CURRENT = new Profile(profile, features); return CURRENT; } private static boolean isEnabledByDefault(ProfileName profile, Feature f) { switch (f.getType()) { case DEFAULT: return true; case PREVIEW: return profile.equals(ProfileName.PREVIEW); default: return false; } } private static ProfileConfigResolver.FeatureConfig getFeatureConfig(String feature, ProfileConfigResolver... resolvers) { ProfileConfigResolver.FeatureConfig configuration = Arrays.stream(resolvers).map(r -> r.getFeatureConfig(feature)) .filter(r -> !r.equals(ProfileConfigResolver.FeatureConfig.UNCONFIGURED)) .findFirst() .orElse(ProfileConfigResolver.FeatureConfig.UNCONFIGURED); return configuration; } /** * Compute a map of unversioned feature keys to ordered sets (highest first) of features. The priority order for features is: *

*

    *
  • The highest default supported version *
  • The highest non-default supported version *
  • The highest deprecated version *
  • The highest preview version *
  • The highest experimental version *
      *

      * Note the {@link Type} enum is ordered based upon priority. */ private static Map> getOrderedFeatures() { if (FEATURES == null) { // "natural" ordering low to high between two features Comparator comparator = Comparator.comparing(Feature::getType).thenComparingInt(Feature::getVersion); // aggregate the features by unversioned key HashMap> features = new HashMap<>(); Stream.of(Feature.values()).forEach(f -> features.compute(f.getUnversionedKey(), (k, v) -> { if (v == null) { v = new TreeSet<>(comparator.reversed()); // we want the highest priority first } v.add(f); return v; })); FEATURES = features; } return FEATURES; } public static Set getAllUnversionedFeatureNames() { return Collections.unmodifiableSet(getOrderedFeatures().keySet()); } public static Set getDisableableUnversionedFeatureNames() { return getOrderedFeatures().keySet().stream().filter(f -> !ESSENTIAL_FEATURES.contains(f)).collect(Collectors.toSet()); } /** * Get all of the feature versions for the given feature. They will be ordered by priority. *

      * If the feature does not exist an empty collection will be returned. */ public static Set getFeatureVersions(String feature) { TreeSet versions = getOrderedFeatures().get(feature); if (versions == null) { return Collections.emptySet(); } return Collections.unmodifiableSet(versions); } public static Profile init(ProfileName profileName, Map features) { CURRENT = new Profile(profileName, features); return CURRENT; } private Profile(ProfileName profileName, Map features) { this.profileName = profileName; this.features = Collections.unmodifiableMap(features); logUnsupportedFeatures(); } public static Profile getInstance() { return CURRENT; } public static boolean isFeatureEnabled(Feature feature) { return getInstance().features.get(feature); } public ProfileName getName() { return profileName; } public Set getAllFeatures() { return features.keySet(); } public Set getDisabledFeatures() { return features.entrySet().stream().filter(e -> !e.getValue()).map(Map.Entry::getKey).collect(Collectors.toSet()); } /** * @return all features of type "preview" or "preview_disabled_by_default" */ public Set getPreviewFeatures() { return Stream.concat(getFeatures(Feature.Type.PREVIEW).stream(), getFeatures(Feature.Type.PREVIEW_DISABLED_BY_DEFAULT).stream()) .collect(Collectors.toSet()); } public Set getExperimentalFeatures() { return getFeatures(Feature.Type.EXPERIMENTAL); } public Set getDeprecatedFeatures() { return getFeatures(Feature.Type.DEPRECATED); } public Set getFeatures(Feature.Type type) { return features.keySet().stream().filter(f -> f.getType().equals(type)).collect(Collectors.toSet()); } public Map getFeatures() { return features; } public enum ProfileName { DEFAULT, PREVIEW } private static void verifyConfig(Map features) { for (Feature f : features.keySet()) { if (features.get(f) && f.getDependencies() != null) { for (Feature d : f.getDependencies()) { if (!features.get(d)) { throw new ProfileException("Feature " + f.getKey() + " depends on disabled feature " + d.getKey()); } } } } } private void logUnsupportedFeatures() { logUnsupportedFeatures(Feature.Type.PREVIEW, getPreviewFeatures(), Logger.Level.INFO); logUnsupportedFeatures(Feature.Type.EXPERIMENTAL, getExperimentalFeatures(), Logger.Level.WARN); logUnsupportedFeatures(Feature.Type.DEPRECATED, getDeprecatedFeatures(), Logger.Level.WARN); } private void logUnsupportedFeatures(Feature.Type type, Set checkedFeatures, Logger.Level level) { Set checkedFeatureTypes = checkedFeatures.stream() .map(Feature::getType) .collect(Collectors.toSet()); String enabledFeaturesOfType = features.entrySet().stream() .filter(e -> e.getValue() && checkedFeatureTypes.contains(e.getKey().getType())) .map(e -> e.getKey().getVersionedKey()).sorted().collect(Collectors.joining(", ")); if (!enabledFeaturesOfType.isEmpty()) { logger.logv(level, "{0} features enabled: {1}", type.getLabel(), enabledFeaturesOfType); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy