diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientTypeRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientTypeRepresentation.java index 1f3362e832..8170118edb 100644 --- a/core/src/main/java/org/keycloak/representations/idm/ClientTypeRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ClientTypeRepresentation.java @@ -33,6 +33,9 @@ public class ClientTypeRepresentation { @JsonProperty("provider") private String provider; + @JsonProperty("parent") + private String parent; + @JsonProperty("config") private Map config; @@ -60,6 +63,14 @@ public class ClientTypeRepresentation { this.config = config; } + public String getParent() { + return parent; + } + + public void setParent(String parent) { + this.parent = parent; + } + public static class PropertyConfig { @JsonProperty("applicable") @@ -85,4 +96,4 @@ public class ClientTypeRepresentation { this.value = value; } } -} \ No newline at end of file +} diff --git a/server-spi-private/src/main/java/org/keycloak/client/clienttype/ClientType.java b/server-spi-private/src/main/java/org/keycloak/client/clienttype/ClientType.java index bfe9935eec..f017f004d6 100644 --- a/server-spi-private/src/main/java/org/keycloak/client/clienttype/ClientType.java +++ b/server-spi-private/src/main/java/org/keycloak/client/clienttype/ClientType.java @@ -42,4 +42,4 @@ public interface ClientType { // Augment at the client type ClientModel augment(ClientModel client); -} \ No newline at end of file +} diff --git a/server-spi-private/src/main/java/org/keycloak/client/clienttype/ClientTypeException.java b/server-spi-private/src/main/java/org/keycloak/client/clienttype/ClientTypeException.java index e03a518d78..dc6734f182 100644 --- a/server-spi-private/src/main/java/org/keycloak/client/clienttype/ClientTypeException.java +++ b/server-spi-private/src/main/java/org/keycloak/client/clienttype/ClientTypeException.java @@ -29,10 +29,6 @@ public class ClientTypeException extends ModelException { super(message, parameters); } - private ClientTypeException(String message, Throwable cause) { - super(message, cause); - } - public enum Message { /** * Register all client type exception messages through this enum to keep things consistent across the services. @@ -57,10 +53,6 @@ public class ClientTypeException extends ModelException { return new ClientTypeException(message, parameters); } - public ClientTypeException exception(String message, Throwable cause) { - return new ClientTypeException(message, cause); - } - public String getMessage() { return message; } diff --git a/server-spi-private/src/main/java/org/keycloak/client/clienttype/ClientTypeProvider.java b/server-spi-private/src/main/java/org/keycloak/client/clienttype/ClientTypeProvider.java index 90584a4e15..5f43c5a574 100644 --- a/server-spi-private/src/main/java/org/keycloak/client/clienttype/ClientTypeProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/client/clienttype/ClientTypeProvider.java @@ -27,7 +27,7 @@ import org.keycloak.representations.idm.ClientTypeRepresentation; public interface ClientTypeProvider extends Provider { // Return client types for the model returned - ClientType getClientType(ClientTypeRepresentation clientTypeRep); + ClientType getClientType(ClientTypeRepresentation clientTypeRep, ClientType parent); // TODO:client-types type-safety here. The returned clientType should have correctly casted client type configuration // Used when creating/updating clientType. The JSON configuration is validated to be checked if it matches the good format for client type @@ -37,4 +37,4 @@ public interface ClientTypeProvider extends Provider { default void close() { } -} \ No newline at end of file +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index a2a9eac23f..f433f81581 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -27,7 +27,6 @@ import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; @@ -655,8 +654,8 @@ public class RepresentationToModel { } /** - * Create Supplier to update property, if not null. - * Captures {@link ClientTypeException} if thrown by the setter. + * Create Supplier to update property. + * Captures and returns {@link ClientTypeException} if thrown by the setter. * * @param modelSetter setter to call. * @param representationGetter getter supplying the property update. @@ -687,9 +686,7 @@ public class RepresentationToModel { private static Supplier updatePropertyAction(Consumer modelSetter, Supplier... getters) { Stream firstNonNullSupplied = Stream.of(getters) .map(Supplier::get) - .map(Optional::ofNullable) - .filter(Optional::isPresent) - .map(Optional::get); + .filter(Objects::nonNull); return updateProperty(modelSetter, () -> firstNonNullSupplied.findFirst().orElse(null)); } diff --git a/services/src/main/java/org/keycloak/services/clienttype/DefaultClientTypeManager.java b/services/src/main/java/org/keycloak/services/clienttype/DefaultClientTypeManager.java index ea406545b7..56e16a4240 100644 --- a/services/src/main/java/org/keycloak/services/clienttype/DefaultClientTypeManager.java +++ b/services/src/main/java/org/keycloak/services/clienttype/DefaultClientTypeManager.java @@ -21,6 +21,8 @@ package org.keycloak.services.clienttype; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -102,8 +104,13 @@ public class DefaultClientTypeManager implements ClientTypeManager { throw ClientTypeException.Message.CLIENT_TYPE_NOT_FOUND.exception(); } + ClientType parent = null; + if (clientType.getParent() != null) { + parent = getClientType(realm, clientType.getParent()); + } + ClientTypeProvider provider = session.getProvider(ClientTypeProvider.class, clientType.getProvider()); - return provider.getClientType(clientType); + return provider.getClientType(clientType, parent); } @Override @@ -173,4 +180,4 @@ public class DefaultClientTypeManager implements ClientTypeManager { } return null; } -} \ No newline at end of file +} diff --git a/services/src/main/java/org/keycloak/services/clienttype/client/TypeAwareClientModelDelegate.java b/services/src/main/java/org/keycloak/services/clienttype/client/TypeAwareClientModelDelegate.java index fe1174c154..5e44aaed92 100644 --- a/services/src/main/java/org/keycloak/services/clienttype/client/TypeAwareClientModelDelegate.java +++ b/services/src/main/java/org/keycloak/services/clienttype/client/TypeAwareClientModelDelegate.java @@ -49,7 +49,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate { @Override public boolean isStandardFlowEnabled() { return TypedClientSimpleAttribute.STANDARD_FLOW_ENABLED - .getClientAttribute(clientType, Boolean.class); + .getClientAttribute(clientType, super::isStandardFlowEnabled, Boolean.class); } @Override @@ -61,7 +61,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate { @Override public boolean isBearerOnly() { return TypedClientSimpleAttribute.BEARER_ONLY - .getClientAttribute(clientType, Boolean.class); + .getClientAttribute(clientType, super::isBearerOnly, Boolean.class); } @Override @@ -73,7 +73,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate { @Override public boolean isConsentRequired() { return TypedClientSimpleAttribute.CONSENT_REQUIRED - .getClientAttribute(clientType, Boolean.class); + .getClientAttribute(clientType, super::isConsentRequired, Boolean.class); } @Override @@ -85,7 +85,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate { @Override public boolean isDirectAccessGrantsEnabled() { return TypedClientSimpleAttribute.DIRECT_ACCESS_GRANTS_ENABLED - .getClientAttribute(clientType, Boolean.class); + .getClientAttribute(clientType, super::isDirectAccessGrantsEnabled, Boolean.class); } @Override @@ -97,7 +97,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate { @Override public boolean isAlwaysDisplayInConsole() { return TypedClientSimpleAttribute.ALWAYS_DISPLAY_IN_CONSOLE - .getClientAttribute(clientType, Boolean.class); + .getClientAttribute(clientType, super::isAlwaysDisplayInConsole, Boolean.class); } @Override @@ -109,7 +109,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate { @Override public boolean isFrontchannelLogout() { return TypedClientSimpleAttribute.FRONTCHANNEL_LOGOUT - .getClientAttribute(clientType, Boolean.class); + .getClientAttribute(clientType, super::isFrontchannelLogout, Boolean.class); } @Override @@ -121,7 +121,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate { @Override public boolean isImplicitFlowEnabled() { return TypedClientSimpleAttribute.IMPLICIT_FLOW_ENABLED - .getClientAttribute(clientType, Boolean.class); + .getClientAttribute(clientType, super::isImplicitFlowEnabled, Boolean.class); } @Override @@ -133,7 +133,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate { @Override public boolean isServiceAccountsEnabled() { return TypedClientSimpleAttribute.SERVICE_ACCOUNTS_ENABLED - .getClientAttribute(clientType, Boolean.class); + .getClientAttribute(clientType, super::isServiceAccountsEnabled, Boolean.class); } @Override @@ -145,7 +145,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate { @Override public String getProtocol() { return TypedClientSimpleAttribute.PROTOCOL - .getClientAttribute(clientType, String.class); + .getClientAttribute(clientType, super::getProtocol, String.class); } @Override @@ -157,7 +157,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate { @Override public boolean isPublicClient() { return TypedClientSimpleAttribute.PUBLIC_CLIENT - .getClientAttribute(clientType, Boolean.class); + .getClientAttribute(clientType, super::isPublicClient, Boolean.class); } @Override @@ -169,7 +169,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate { @Override public Set getWebOrigins() { return TypedClientSimpleAttribute.WEB_ORIGINS - .getClientAttribute(clientType, Set.class); + .getClientAttribute(clientType, super::getWebOrigins, Set.class); } @Override @@ -193,7 +193,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate { @Override public Set getRedirectUris() { return TypedClientSimpleAttribute.REDIRECT_URIS - .getClientAttribute(clientType, Set.class); + .getClientAttribute(clientType, super::getRedirectUris, Set.class); } @Override @@ -238,7 +238,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate { public String getAttribute(String name) { TypedClientExtendedAttribute attribute = TypedClientExtendedAttribute.getAttributesByName().get(name); if (attribute != null) { - return attribute.getClientAttribute(clientType, String.class); + return attribute.getClientAttribute(clientType, () -> super.getAttribute(name), String.class); } else { return super.getAttribute(name); } @@ -252,8 +252,8 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate { // Get extended client type attributes and values from the client type configuration. Set extendedClientTypeAttributes = clientType.getOptionNames().stream() - .filter(optionName -> TypedClientExtendedAttribute.getAttributesByName().containsKey(optionName)) - .collect(Collectors.toSet()); + .filter(optionName -> TypedClientExtendedAttribute.getAttributesByName().containsKey(optionName)) + .collect(Collectors.toSet()); // Augment client type attributes on top of attributes on the delegate. for (String entry : extendedClientTypeAttributes) { diff --git a/services/src/main/java/org/keycloak/services/clienttype/client/TypedClientAttribute.java b/services/src/main/java/org/keycloak/services/clienttype/client/TypedClientAttribute.java index 18c469e207..6406f370e3 100644 --- a/services/src/main/java/org/keycloak/services/clienttype/client/TypedClientAttribute.java +++ b/services/src/main/java/org/keycloak/services/clienttype/client/TypedClientAttribute.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; enum TypedClientSimpleAttribute implements TypedClientAttribute { // Top Level client attributes @@ -120,7 +121,7 @@ enum TypedClientExtendedAttribute implements TypedClientAttribute { interface TypedClientAttribute { Logger logger = Logger.getLogger(TypedClientAttribute.class); - default T getClientAttribute(ClientType clientType, Class tClass) { + default T getClientAttribute(ClientType clientType, Supplier clientGetter, Class tClass) { String propertyName = getPropertyName(); Object nonApplicableValue = getNonApplicableValue(); @@ -134,7 +135,9 @@ interface TypedClientAttribute { } } - return clientType.getTypeValue(propertyName, tClass); + T typeValue = clientType.getTypeValue(propertyName, tClass); + // If the value is not supplied by the client type, delegate to the client getter. + return typeValue == null ? clientGetter.get() : typeValue; } default void setClientAttribute(ClientType clientType, T newValue, Consumer clientSetter, Class tClass) { @@ -148,8 +151,8 @@ interface TypedClientAttribute { } // If there is an attempt to change a value for an applicable field with a read-only value set, then throw an exception. - T oldVal = clientType.getTypeValue(propertyName, tClass); - if (!ObjectUtil.isEqualOrBothNull(oldVal, newValue)) { + T readOnlyValue = clientType.getTypeValue(propertyName, tClass); + if (readOnlyValue != null && !readOnlyValue.equals(newValue)) { throw ClientTypeException.Message.CLIENT_UPDATE_FAILED_CLIENT_TYPE_VALIDATION.exception(propertyName); } diff --git a/services/src/main/java/org/keycloak/services/clienttype/impl/DefaultClientType.java b/services/src/main/java/org/keycloak/services/clienttype/impl/DefaultClientType.java index 6ebd3a3489..681d4b97d8 100644 --- a/services/src/main/java/org/keycloak/services/clienttype/impl/DefaultClientType.java +++ b/services/src/main/java/org/keycloak/services/clienttype/impl/DefaultClientType.java @@ -23,8 +23,9 @@ import org.keycloak.models.ClientModel; import org.keycloak.representations.idm.ClientTypeRepresentation; import org.keycloak.services.clienttype.client.TypeAwareClientModelDelegate; -import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author Marek Posolda @@ -32,9 +33,11 @@ import java.util.Set; public class DefaultClientType implements ClientType { private final ClientTypeRepresentation clientType; + private final ClientType parentClientType; - public DefaultClientType(ClientTypeRepresentation clientType) { + public DefaultClientType(ClientTypeRepresentation clientType, ClientType parentClientType) { this.clientType = clientType; + this.parentClientType = parentClientType; } @Override @@ -44,31 +47,40 @@ public class DefaultClientType implements ClientType { @Override public boolean isApplicable(String optionName) { - // Each property is applicable by default if not configured for the particular client type - return getConfiguration(optionName) - .map(ClientTypeRepresentation.PropertyConfig::getApplicable) - .orElse(true); + ClientTypeRepresentation.PropertyConfig propertyConfig = clientType.getConfig().get(optionName); + if (propertyConfig != null) { + return propertyConfig.getApplicable(); + } + + if (parentClientType != null) { + return parentClientType.isApplicable(optionName); + } + + return true; } @Override public T getTypeValue(String optionName, Class optionType) { - - return getConfiguration(optionName) - .map(ClientTypeRepresentation.PropertyConfig::getValue) - .map(optionType::cast).orElse(null); + ClientTypeRepresentation.PropertyConfig propertyConfig = clientType.getConfig().get(optionName); + if (propertyConfig != null) { + return optionType.cast(propertyConfig.getValue()); + } else if (parentClientType != null) { + return parentClientType.getTypeValue(optionName, optionType); + } + return null; } @Override public Set getOptionNames() { - return clientType.getConfig().keySet(); + Stream optionNames = clientType.getConfig().keySet().stream(); + if (parentClientType != null) { + optionNames = Stream.concat(optionNames, parentClientType.getOptionNames().stream()); + } + return optionNames.collect(Collectors.toSet()); } @Override public ClientModel augment(ClientModel client) { return new TypeAwareClientModelDelegate(this, () -> client); } - - private Optional getConfiguration(String optionName) { - return Optional.ofNullable(clientType.getConfig().get(optionName)); - } -} \ No newline at end of file +} diff --git a/services/src/main/java/org/keycloak/services/clienttype/impl/DefaultClientTypeProvider.java b/services/src/main/java/org/keycloak/services/clienttype/impl/DefaultClientTypeProvider.java index 8d1f1257f0..90d44444e6 100644 --- a/services/src/main/java/org/keycloak/services/clienttype/impl/DefaultClientTypeProvider.java +++ b/services/src/main/java/org/keycloak/services/clienttype/impl/DefaultClientTypeProvider.java @@ -34,8 +34,8 @@ public class DefaultClientTypeProvider implements ClientTypeProvider { private static final Logger logger = Logger.getLogger(DefaultClientTypeProvider.class); @Override - public ClientType getClientType(ClientTypeRepresentation clientTypeRep) { - return new DefaultClientType(clientTypeRep); + public ClientType getClientType(ClientTypeRepresentation clientTypeRep, ClientType parent) { + return new DefaultClientType(clientTypeRep, parent); } @Override @@ -60,4 +60,4 @@ public class DefaultClientTypeProvider implements ClientTypeProvider { // TODO:client-types retype configuration return clientType; } -} \ No newline at end of file +} diff --git a/services/src/main/resources/keycloak-default-client-types.json b/services/src/main/resources/keycloak-default-client-types.json index 566398f0ee..d95288bb01 100644 --- a/services/src/main/resources/keycloak-default-client-types.json +++ b/services/src/main/resources/keycloak-default-client-types.json @@ -14,6 +14,9 @@ "name": "oidc", "provider": "default", "config": { + "alwaysDisplayInConsole": { + "applicable": true + }, "authorizationServicesEnabled": { "applicable": true }, @@ -59,6 +62,7 @@ { "name": "service-account", "provider": "default", + "parent": "oidc", "config": { "alwaysDisplayInConsole": { "applicable": false @@ -97,10 +101,6 @@ "policyUri": { "applicable": false }, - "protocol": { - "applicable": true, - "value": "openid-connect" - }, "publicClient": { "applicable": true, "value": false diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientTypesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientTypesTest.java index 6bc57cc048..0f39b944a6 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientTypesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientTypesTest.java @@ -42,6 +42,7 @@ import org.keycloak.testsuite.util.ClientBuilder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; @@ -266,6 +267,44 @@ public class ClientTypesTest extends AbstractTestRealmKeycloakTest { assertNames(clientTypes.getGlobalClientTypes(), "sla", "service-account"); } + @Test + public void testClientTypesInheritFromParent() { + ClientTypesRepresentation clientTypes = testRealm().clientTypes().getClientTypes(); + + ClientTypeRepresentation.PropertyConfig applicableAndTrue = new ClientTypeRepresentation.PropertyConfig(); + applicableAndTrue.setApplicable(true); + applicableAndTrue.setValue(true); + + ClientTypeRepresentation childClientType = new ClientTypeRepresentation(); + childClientType.setName("child"); + childClientType.setProvider("default"); + childClientType.setParent("oidc"); + childClientType.setConfig(Map.of("standardFlowEnabled", applicableAndTrue)); + + ClientTypeRepresentation subClientType = new ClientTypeRepresentation(); + subClientType.setName("subClientType"); + subClientType.setProvider("default"); + subClientType.setParent("child"); + subClientType.setConfig(Map.of("consentRequired", applicableAndTrue)); + + List realmClientTypes = clientTypes.getRealmClientTypes(); + realmClientTypes.add(childClientType); + realmClientTypes.add(subClientType); + clientTypes.setRealmClientTypes(realmClientTypes); + + testRealm().clientTypes().updateClientTypes(clientTypes); + + ClientRepresentation childClient = createClientWithType("child-client", childClientType.getName()); + assertEquals(childClient.getProtocol(), "openid-connect"); + assertEquals(childClient.isStandardFlowEnabled(), true); + assertEquals(childClient.isConsentRequired(), false); + + ClientRepresentation subClient = createClientWithType("sub-client", subClientType.getName()); + assertEquals(subClient.getProtocol(), "openid-connect"); + assertEquals(subClient.isStandardFlowEnabled(), true); + assertEquals(subClient.isConsentRequired(), true); + } + private void assertErrorResponseContainsParams(Response response, String... items) { assertEquals(Response.Status.BAD_REQUEST, response.getStatusInfo()); ErrorRepresentation errorRepresentation = response.readEntity(ErrorRepresentation.class);