Client type configuration inheritance (#30056)

closes #30213 

Signed-off-by: Patrick Jennings <pajennin@redhat.com>
This commit is contained in:
Patrick Jennings 2024-06-10 12:59:08 -04:00 committed by GitHub
parent 7d05a7a013
commit 75925dcf6c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 123 additions and 62 deletions

View file

@ -33,6 +33,9 @@ public class ClientTypeRepresentation {
@JsonProperty("provider") @JsonProperty("provider")
private String provider; private String provider;
@JsonProperty("parent")
private String parent;
@JsonProperty("config") @JsonProperty("config")
private Map<String, PropertyConfig> config; private Map<String, PropertyConfig> config;
@ -60,6 +63,14 @@ public class ClientTypeRepresentation {
this.config = config; this.config = config;
} }
public String getParent() {
return parent;
}
public void setParent(String parent) {
this.parent = parent;
}
public static class PropertyConfig { public static class PropertyConfig {
@JsonProperty("applicable") @JsonProperty("applicable")

View file

@ -29,10 +29,6 @@ public class ClientTypeException extends ModelException {
super(message, parameters); super(message, parameters);
} }
private ClientTypeException(String message, Throwable cause) {
super(message, cause);
}
public enum Message { public enum Message {
/** /**
* Register all client type exception messages through this enum to keep things consistent across the services. * 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); return new ClientTypeException(message, parameters);
} }
public ClientTypeException exception(String message, Throwable cause) {
return new ClientTypeException(message, cause);
}
public String getMessage() { public String getMessage() {
return message; return message;
} }

View file

@ -27,7 +27,7 @@ import org.keycloak.representations.idm.ClientTypeRepresentation;
public interface ClientTypeProvider extends Provider { public interface ClientTypeProvider extends Provider {
// Return client types for the model returned // 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 // 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 // Used when creating/updating clientType. The JSON configuration is validated to be checked if it matches the good format for client type

View file

@ -27,7 +27,6 @@ import java.util.ListIterator;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
@ -655,8 +654,8 @@ public class RepresentationToModel {
} }
/** /**
* Create Supplier to update property, if not null. * Create Supplier to update property.
* Captures {@link ClientTypeException} if thrown by the setter. * Captures and returns {@link ClientTypeException} if thrown by the setter.
* *
* @param modelSetter setter to call. * @param modelSetter setter to call.
* @param representationGetter getter supplying the property update. * @param representationGetter getter supplying the property update.
@ -687,9 +686,7 @@ public class RepresentationToModel {
private static <T> Supplier<ClientTypeException> updatePropertyAction(Consumer<T> modelSetter, Supplier<T>... getters) { private static <T> Supplier<ClientTypeException> updatePropertyAction(Consumer<T> modelSetter, Supplier<T>... getters) {
Stream<T> firstNonNullSupplied = Stream.of(getters) Stream<T> firstNonNullSupplied = Stream.of(getters)
.map(Supplier::get) .map(Supplier::get)
.map(Optional::ofNullable) .filter(Objects::nonNull);
.filter(Optional::isPresent)
.map(Optional::get);
return updateProperty(modelSetter, () -> firstNonNullSupplied.findFirst().orElse(null)); return updateProperty(modelSetter, () -> firstNonNullSupplied.findFirst().orElse(null));
} }

View file

@ -21,6 +21,8 @@ package org.keycloak.services.clienttype;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -102,8 +104,13 @@ public class DefaultClientTypeManager implements ClientTypeManager {
throw ClientTypeException.Message.CLIENT_TYPE_NOT_FOUND.exception(); 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()); ClientTypeProvider provider = session.getProvider(ClientTypeProvider.class, clientType.getProvider());
return provider.getClientType(clientType); return provider.getClientType(clientType, parent);
} }
@Override @Override

View file

@ -49,7 +49,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate {
@Override @Override
public boolean isStandardFlowEnabled() { public boolean isStandardFlowEnabled() {
return TypedClientSimpleAttribute.STANDARD_FLOW_ENABLED return TypedClientSimpleAttribute.STANDARD_FLOW_ENABLED
.getClientAttribute(clientType, Boolean.class); .getClientAttribute(clientType, super::isStandardFlowEnabled, Boolean.class);
} }
@Override @Override
@ -61,7 +61,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate {
@Override @Override
public boolean isBearerOnly() { public boolean isBearerOnly() {
return TypedClientSimpleAttribute.BEARER_ONLY return TypedClientSimpleAttribute.BEARER_ONLY
.getClientAttribute(clientType, Boolean.class); .getClientAttribute(clientType, super::isBearerOnly, Boolean.class);
} }
@Override @Override
@ -73,7 +73,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate {
@Override @Override
public boolean isConsentRequired() { public boolean isConsentRequired() {
return TypedClientSimpleAttribute.CONSENT_REQUIRED return TypedClientSimpleAttribute.CONSENT_REQUIRED
.getClientAttribute(clientType, Boolean.class); .getClientAttribute(clientType, super::isConsentRequired, Boolean.class);
} }
@Override @Override
@ -85,7 +85,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate {
@Override @Override
public boolean isDirectAccessGrantsEnabled() { public boolean isDirectAccessGrantsEnabled() {
return TypedClientSimpleAttribute.DIRECT_ACCESS_GRANTS_ENABLED return TypedClientSimpleAttribute.DIRECT_ACCESS_GRANTS_ENABLED
.getClientAttribute(clientType, Boolean.class); .getClientAttribute(clientType, super::isDirectAccessGrantsEnabled, Boolean.class);
} }
@Override @Override
@ -97,7 +97,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate {
@Override @Override
public boolean isAlwaysDisplayInConsole() { public boolean isAlwaysDisplayInConsole() {
return TypedClientSimpleAttribute.ALWAYS_DISPLAY_IN_CONSOLE return TypedClientSimpleAttribute.ALWAYS_DISPLAY_IN_CONSOLE
.getClientAttribute(clientType, Boolean.class); .getClientAttribute(clientType, super::isAlwaysDisplayInConsole, Boolean.class);
} }
@Override @Override
@ -109,7 +109,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate {
@Override @Override
public boolean isFrontchannelLogout() { public boolean isFrontchannelLogout() {
return TypedClientSimpleAttribute.FRONTCHANNEL_LOGOUT return TypedClientSimpleAttribute.FRONTCHANNEL_LOGOUT
.getClientAttribute(clientType, Boolean.class); .getClientAttribute(clientType, super::isFrontchannelLogout, Boolean.class);
} }
@Override @Override
@ -121,7 +121,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate {
@Override @Override
public boolean isImplicitFlowEnabled() { public boolean isImplicitFlowEnabled() {
return TypedClientSimpleAttribute.IMPLICIT_FLOW_ENABLED return TypedClientSimpleAttribute.IMPLICIT_FLOW_ENABLED
.getClientAttribute(clientType, Boolean.class); .getClientAttribute(clientType, super::isImplicitFlowEnabled, Boolean.class);
} }
@Override @Override
@ -133,7 +133,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate {
@Override @Override
public boolean isServiceAccountsEnabled() { public boolean isServiceAccountsEnabled() {
return TypedClientSimpleAttribute.SERVICE_ACCOUNTS_ENABLED return TypedClientSimpleAttribute.SERVICE_ACCOUNTS_ENABLED
.getClientAttribute(clientType, Boolean.class); .getClientAttribute(clientType, super::isServiceAccountsEnabled, Boolean.class);
} }
@Override @Override
@ -145,7 +145,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate {
@Override @Override
public String getProtocol() { public String getProtocol() {
return TypedClientSimpleAttribute.PROTOCOL return TypedClientSimpleAttribute.PROTOCOL
.getClientAttribute(clientType, String.class); .getClientAttribute(clientType, super::getProtocol, String.class);
} }
@Override @Override
@ -157,7 +157,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate {
@Override @Override
public boolean isPublicClient() { public boolean isPublicClient() {
return TypedClientSimpleAttribute.PUBLIC_CLIENT return TypedClientSimpleAttribute.PUBLIC_CLIENT
.getClientAttribute(clientType, Boolean.class); .getClientAttribute(clientType, super::isPublicClient, Boolean.class);
} }
@Override @Override
@ -169,7 +169,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate {
@Override @Override
public Set<String> getWebOrigins() { public Set<String> getWebOrigins() {
return TypedClientSimpleAttribute.WEB_ORIGINS return TypedClientSimpleAttribute.WEB_ORIGINS
.getClientAttribute(clientType, Set.class); .getClientAttribute(clientType, super::getWebOrigins, Set.class);
} }
@Override @Override
@ -193,7 +193,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate {
@Override @Override
public Set<String> getRedirectUris() { public Set<String> getRedirectUris() {
return TypedClientSimpleAttribute.REDIRECT_URIS return TypedClientSimpleAttribute.REDIRECT_URIS
.getClientAttribute(clientType, Set.class); .getClientAttribute(clientType, super::getRedirectUris, Set.class);
} }
@Override @Override
@ -238,7 +238,7 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate {
public String getAttribute(String name) { public String getAttribute(String name) {
TypedClientExtendedAttribute attribute = TypedClientExtendedAttribute.getAttributesByName().get(name); TypedClientExtendedAttribute attribute = TypedClientExtendedAttribute.getAttributesByName().get(name);
if (attribute != null) { if (attribute != null) {
return attribute.getClientAttribute(clientType, String.class); return attribute.getClientAttribute(clientType, () -> super.getAttribute(name), String.class);
} else { } else {
return super.getAttribute(name); 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. // Get extended client type attributes and values from the client type configuration.
Set<String> extendedClientTypeAttributes = Set<String> extendedClientTypeAttributes =
clientType.getOptionNames().stream() clientType.getOptionNames().stream()
.filter(optionName -> TypedClientExtendedAttribute.getAttributesByName().containsKey(optionName)) .filter(optionName -> TypedClientExtendedAttribute.getAttributesByName().containsKey(optionName))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
// Augment client type attributes on top of attributes on the delegate. // Augment client type attributes on top of attributes on the delegate.
for (String entry : extendedClientTypeAttributes) { for (String entry : extendedClientTypeAttributes) {

View file

@ -11,6 +11,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier;
enum TypedClientSimpleAttribute implements TypedClientAttribute { enum TypedClientSimpleAttribute implements TypedClientAttribute {
// Top Level client attributes // Top Level client attributes
@ -120,7 +121,7 @@ enum TypedClientExtendedAttribute implements TypedClientAttribute {
interface TypedClientAttribute { interface TypedClientAttribute {
Logger logger = Logger.getLogger(TypedClientAttribute.class); Logger logger = Logger.getLogger(TypedClientAttribute.class);
default <T> T getClientAttribute(ClientType clientType, Class<T> tClass) { default <T> T getClientAttribute(ClientType clientType, Supplier<T> clientGetter, Class<T> tClass) {
String propertyName = getPropertyName(); String propertyName = getPropertyName();
Object nonApplicableValue = getNonApplicableValue(); 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 <T> void setClientAttribute(ClientType clientType, T newValue, Consumer<T> clientSetter, Class<T> tClass) { default <T> void setClientAttribute(ClientType clientType, T newValue, Consumer<T> clientSetter, Class<T> 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. // 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); T readOnlyValue = clientType.getTypeValue(propertyName, tClass);
if (!ObjectUtil.isEqualOrBothNull(oldVal, newValue)) { if (readOnlyValue != null && !readOnlyValue.equals(newValue)) {
throw ClientTypeException.Message.CLIENT_UPDATE_FAILED_CLIENT_TYPE_VALIDATION.exception(propertyName); throw ClientTypeException.Message.CLIENT_UPDATE_FAILED_CLIENT_TYPE_VALIDATION.exception(propertyName);
} }

View file

@ -23,8 +23,9 @@ import org.keycloak.models.ClientModel;
import org.keycloak.representations.idm.ClientTypeRepresentation; import org.keycloak.representations.idm.ClientTypeRepresentation;
import org.keycloak.services.clienttype.client.TypeAwareClientModelDelegate; import org.keycloak.services.clienttype.client.TypeAwareClientModelDelegate;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -32,9 +33,11 @@ import java.util.Set;
public class DefaultClientType implements ClientType { public class DefaultClientType implements ClientType {
private final ClientTypeRepresentation clientType; private final ClientTypeRepresentation clientType;
private final ClientType parentClientType;
public DefaultClientType(ClientTypeRepresentation clientType) { public DefaultClientType(ClientTypeRepresentation clientType, ClientType parentClientType) {
this.clientType = clientType; this.clientType = clientType;
this.parentClientType = parentClientType;
} }
@Override @Override
@ -44,31 +47,40 @@ public class DefaultClientType implements ClientType {
@Override @Override
public boolean isApplicable(String optionName) { public boolean isApplicable(String optionName) {
// Each property is applicable by default if not configured for the particular client type ClientTypeRepresentation.PropertyConfig propertyConfig = clientType.getConfig().get(optionName);
return getConfiguration(optionName) if (propertyConfig != null) {
.map(ClientTypeRepresentation.PropertyConfig::getApplicable) return propertyConfig.getApplicable();
.orElse(true); }
if (parentClientType != null) {
return parentClientType.isApplicable(optionName);
}
return true;
} }
@Override @Override
public <T> T getTypeValue(String optionName, Class<T> optionType) { public <T> T getTypeValue(String optionName, Class<T> optionType) {
ClientTypeRepresentation.PropertyConfig propertyConfig = clientType.getConfig().get(optionName);
return getConfiguration(optionName) if (propertyConfig != null) {
.map(ClientTypeRepresentation.PropertyConfig::getValue) return optionType.cast(propertyConfig.getValue());
.map(optionType::cast).orElse(null); } else if (parentClientType != null) {
return parentClientType.getTypeValue(optionName, optionType);
}
return null;
} }
@Override @Override
public Set<String> getOptionNames() { public Set<String> getOptionNames() {
return clientType.getConfig().keySet(); Stream<String> optionNames = clientType.getConfig().keySet().stream();
if (parentClientType != null) {
optionNames = Stream.concat(optionNames, parentClientType.getOptionNames().stream());
}
return optionNames.collect(Collectors.toSet());
} }
@Override @Override
public ClientModel augment(ClientModel client) { public ClientModel augment(ClientModel client) {
return new TypeAwareClientModelDelegate(this, () -> client); return new TypeAwareClientModelDelegate(this, () -> client);
} }
private Optional<ClientTypeRepresentation.PropertyConfig> getConfiguration(String optionName) {
return Optional.ofNullable(clientType.getConfig().get(optionName));
}
} }

View file

@ -34,8 +34,8 @@ public class DefaultClientTypeProvider implements ClientTypeProvider {
private static final Logger logger = Logger.getLogger(DefaultClientTypeProvider.class); private static final Logger logger = Logger.getLogger(DefaultClientTypeProvider.class);
@Override @Override
public ClientType getClientType(ClientTypeRepresentation clientTypeRep) { public ClientType getClientType(ClientTypeRepresentation clientTypeRep, ClientType parent) {
return new DefaultClientType(clientTypeRep); return new DefaultClientType(clientTypeRep, parent);
} }
@Override @Override

View file

@ -14,6 +14,9 @@
"name": "oidc", "name": "oidc",
"provider": "default", "provider": "default",
"config": { "config": {
"alwaysDisplayInConsole": {
"applicable": true
},
"authorizationServicesEnabled": { "authorizationServicesEnabled": {
"applicable": true "applicable": true
}, },
@ -59,6 +62,7 @@
{ {
"name": "service-account", "name": "service-account",
"provider": "default", "provider": "default",
"parent": "oidc",
"config": { "config": {
"alwaysDisplayInConsole": { "alwaysDisplayInConsole": {
"applicable": false "applicable": false
@ -97,10 +101,6 @@
"policyUri": { "policyUri": {
"applicable": false "applicable": false
}, },
"protocol": {
"applicable": true,
"value": "openid-connect"
},
"publicClient": { "publicClient": {
"applicable": true, "applicable": true,
"value": false "value": false

View file

@ -42,6 +42,7 @@ import org.keycloak.testsuite.util.ClientBuilder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
@ -266,6 +267,44 @@ public class ClientTypesTest extends AbstractTestRealmKeycloakTest {
assertNames(clientTypes.getGlobalClientTypes(), "sla", "service-account"); 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<ClientTypeRepresentation> 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) { private void assertErrorResponseContainsParams(Response response, String... items) {
assertEquals(Response.Status.BAD_REQUEST, response.getStatusInfo()); assertEquals(Response.Status.BAD_REQUEST, response.getStatusInfo());
ErrorRepresentation errorRepresentation = response.readEntity(ErrorRepresentation.class); ErrorRepresentation errorRepresentation = response.readEntity(ErrorRepresentation.class);