Client type configuration inheritance (#30056)
closes #30213 Signed-off-by: Patrick Jennings <pajennin@redhat.com>
This commit is contained in:
parent
7d05a7a013
commit
75925dcf6c
12 changed files with 123 additions and 62 deletions
|
@ -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")
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue