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")
private String provider;
@JsonProperty("parent")
private String parent;
@JsonProperty("config")
private Map<String, PropertyConfig> 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")

View file

@ -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;
}

View file

@ -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

View file

@ -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 <T> Supplier<ClientTypeException> updatePropertyAction(Consumer<T> modelSetter, Supplier<T>... getters) {
Stream<T> 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));
}

View file

@ -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

View file

@ -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<String> 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<String> 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);
}

View file

@ -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> T getClientAttribute(ClientType clientType, Class<T> tClass) {
default <T> T getClientAttribute(ClientType clientType, Supplier<T> clientGetter, Class<T> 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 <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.
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);
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -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> T getTypeValue(String optionName, Class<T> 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<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
public ClientModel augment(ClientModel 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);
@Override
public ClientType getClientType(ClientTypeRepresentation clientTypeRep) {
return new DefaultClientType(clientTypeRep);
public ClientType getClientType(ClientTypeRepresentation clientTypeRep, ClientType parent) {
return new DefaultClientType(clientTypeRep, parent);
}
@Override

View file

@ -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

View file

@ -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<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) {
assertEquals(Response.Status.BAD_REQUEST, response.getStatusInfo());
ErrorRepresentation errorRepresentation = response.readEntity(ErrorRepresentation.class);