Client type OIDC base read only defaults (#29706)
closes #29742 closes #29422 Signed-off-by: Patrick Jennings <pajennin@redhat.com>
This commit is contained in:
parent
68b2e40b38
commit
84acc953dd
10 changed files with 131 additions and 119 deletions
|
@ -20,6 +20,7 @@ package org.keycloak.common.util;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -89,4 +90,8 @@ public class CollectionUtil {
|
||||||
.filter(searchCollection::contains)
|
.filter(searchCollection::contains)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> Set<T> collectionToSet(Collection<T> collection) {
|
||||||
|
return collection == null ? null : new HashSet<>(collection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,11 +65,8 @@ public class ClientTypeRepresentation {
|
||||||
@JsonProperty("applicable")
|
@JsonProperty("applicable")
|
||||||
private Boolean applicable;
|
private Boolean applicable;
|
||||||
|
|
||||||
@JsonProperty("read-only")
|
@JsonProperty("value")
|
||||||
private Boolean readOnly;
|
private Object value;
|
||||||
|
|
||||||
@JsonProperty("default-value")
|
|
||||||
private Object defaultValue;
|
|
||||||
|
|
||||||
public Boolean getApplicable() {
|
public Boolean getApplicable() {
|
||||||
return applicable;
|
return applicable;
|
||||||
|
@ -79,20 +76,13 @@ public class ClientTypeRepresentation {
|
||||||
this.applicable = applicable;
|
this.applicable = applicable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getReadOnly() {
|
|
||||||
return readOnly;
|
public Object getValue() {
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setReadOnly(Boolean readOnly) {
|
public void setValue(Object value) {
|
||||||
this.readOnly = readOnly;
|
this.value = value;
|
||||||
}
|
|
||||||
|
|
||||||
public Object getDefaultValue() {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDefaultValue(Object defaultValue) {
|
|
||||||
this.defaultValue = defaultValue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -19,9 +19,8 @@
|
||||||
package org.keycloak.client.clienttype;
|
package org.keycloak.client.clienttype;
|
||||||
|
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.representations.idm.ClientTypeRepresentation;
|
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO:client-types javadocs
|
* TODO:client-types javadocs
|
||||||
|
@ -36,14 +35,10 @@ public interface ClientType {
|
||||||
// Can be property name (like "standardFlow" or "rootUrl") or attributeName (like "pkceEnabled")
|
// Can be property name (like "standardFlow" or "rootUrl") or attributeName (like "pkceEnabled")
|
||||||
boolean isApplicable(String optionName);
|
boolean isApplicable(String optionName);
|
||||||
|
|
||||||
// Return if option is configurable by clientType or not...
|
|
||||||
boolean isReadOnly(String optionName);
|
|
||||||
|
|
||||||
// Return the value of particular option (if it can be provided by clientType) or return null if this option is not provided by client type
|
// Return the value of particular option (if it can be provided by clientType) or return null if this option is not provided by client type
|
||||||
<T> T getDefaultValue(String optionName, Class<T> optionType);
|
<T> T getTypeValue(String optionName, Class<T> optionType);
|
||||||
|
|
||||||
|
Set<String> getOptionNames();
|
||||||
Map<String, ClientTypeRepresentation.PropertyConfig> getConfiguration();
|
|
||||||
|
|
||||||
// Augment at the client type
|
// Augment at the client type
|
||||||
ClientModel augment(ClientModel client);
|
ClientModel augment(ClientModel client);
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
package org.keycloak.models.utils;
|
package org.keycloak.models.utils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
@ -61,6 +60,7 @@ import org.keycloak.client.clienttype.ClientTypeException;
|
||||||
import org.keycloak.client.clienttype.ClientTypeManager;
|
import org.keycloak.client.clienttype.ClientTypeManager;
|
||||||
import org.keycloak.common.Profile;
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.common.Profile.Feature;
|
import org.keycloak.common.Profile.Feature;
|
||||||
|
import org.keycloak.common.util.CollectionUtil;
|
||||||
import org.keycloak.common.util.MultivaluedHashMap;
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
import org.keycloak.common.util.ObjectUtil;
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
import org.keycloak.common.util.UriUtils;
|
import org.keycloak.common.util.UriUtils;
|
||||||
|
@ -413,7 +413,7 @@ public class RepresentationToModel {
|
||||||
public static void updateClient(ClientRepresentation rep, ClientModel resource, KeycloakSession session) {
|
public static void updateClient(ClientRepresentation rep, ClientModel resource, KeycloakSession session) {
|
||||||
|
|
||||||
if (Profile.isFeatureEnabled(Profile.Feature.CLIENT_TYPES)) {
|
if (Profile.isFeatureEnabled(Profile.Feature.CLIENT_TYPES)) {
|
||||||
if (!ObjectUtil.isEqualOrBothNull(rep.getType(), rep.getType())) {
|
if (!ObjectUtil.isEqualOrBothNull(resource.getType(), rep.getType())) {
|
||||||
throw new ClientTypeException("Not supported to change client type");
|
throw new ClientTypeException("Not supported to change client type");
|
||||||
}
|
}
|
||||||
if (rep.getType() != null) {
|
if (rep.getType() != null) {
|
||||||
|
@ -526,8 +526,8 @@ public class RepresentationToModel {
|
||||||
// Client Secret
|
// Client Secret
|
||||||
add(updatePropertyAction(client::setSecret, () -> determineNewSecret(client, rep)));
|
add(updatePropertyAction(client::setSecret, () -> determineNewSecret(client, rep)));
|
||||||
// Redirect uris / Web origins
|
// Redirect uris / Web origins
|
||||||
add(updatePropertyAction(client::setRedirectUris, () -> collectionToSet(rep.getRedirectUris()), client::getRedirectUris));
|
add(updatePropertyAction(client::setRedirectUris, () -> CollectionUtil.collectionToSet(rep.getRedirectUris()), client::getRedirectUris));
|
||||||
add(updatePropertyAction(client::setWebOrigins, () -> collectionToSet(rep.getWebOrigins()), () -> defaultWebOrigins(client)));
|
add(updatePropertyAction(client::setWebOrigins, () -> CollectionUtil.collectionToSet(rep.getWebOrigins()), () -> defaultWebOrigins(client)));
|
||||||
}};
|
}};
|
||||||
|
|
||||||
// Extended client attributes
|
// Extended client attributes
|
||||||
|
@ -689,6 +689,7 @@ public class RepresentationToModel {
|
||||||
* @return {@link Supplier} with results of operation.
|
* @return {@link Supplier} with results of operation.
|
||||||
* @param <T> Type of property.
|
* @param <T> Type of property.
|
||||||
*/
|
*/
|
||||||
|
@SafeVarargs
|
||||||
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)
|
||||||
|
@ -1645,12 +1646,6 @@ public class RepresentationToModel {
|
||||||
return toModel(representation, authorization, client);
|
return toModel(representation, authorization, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static <T> Set<T> collectionToSet(Collection<T> collection) {
|
|
||||||
return Optional.ofNullable(collection)
|
|
||||||
.map(HashSet::new)
|
|
||||||
.orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void updateOrganizationBroker(RealmModel realm, IdentityProviderRepresentation representation, KeycloakSession session) {
|
private static void updateOrganizationBroker(RealmModel realm, IdentityProviderRepresentation representation, KeycloakSession session) {
|
||||||
if (!Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
if (!Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -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, super::isStandardFlowEnabled, Boolean.class);
|
.getClientAttribute(clientType, 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, super::isBearerOnly, Boolean.class);
|
.getClientAttribute(clientType, 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, super::isConsentRequired, Boolean.class);
|
.getClientAttribute(clientType, 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, super::isDirectAccessGrantsEnabled, Boolean.class);
|
.getClientAttribute(clientType, 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, super::isAlwaysDisplayInConsole, Boolean.class);
|
.getClientAttribute(clientType, 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, super::isFrontchannelLogout, Boolean.class);
|
.getClientAttribute(clientType, 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, super::isImplicitFlowEnabled, Boolean.class);
|
.getClientAttribute(clientType, 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, super::isServiceAccountsEnabled, Boolean.class);
|
.getClientAttribute(clientType, 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, super::getProtocol, String.class);
|
.getClientAttribute(clientType, 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, super::isPublicClient, Boolean.class);
|
.getClientAttribute(clientType, 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, super::getWebOrigins, Set.class);
|
.getClientAttribute(clientType, 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, super::getRedirectUris, Set.class);
|
.getClientAttribute(clientType, 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, () -> super.getAttribute(name), String.class);
|
return attribute.getClientAttribute(clientType, String.class);
|
||||||
} else {
|
} else {
|
||||||
return super.getAttribute(name);
|
return super.getAttribute(name);
|
||||||
}
|
}
|
||||||
|
@ -251,9 +251,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.getConfiguration().entrySet().stream()
|
clientType.getOptionNames().stream()
|
||||||
.map(Map.Entry::getKey)
|
.filter(optionName -> TypedClientExtendedAttribute.getAttributesByName().containsKey(optionName))
|
||||||
.filter(entry -> TypedClientExtendedAttribute.getAttributesByName().containsKey(entry))
|
|
||||||
.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.
|
||||||
|
|
|
@ -11,7 +11,6 @@ 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
|
||||||
|
@ -89,7 +88,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, Supplier<T> clientGetter, Class<T> tClass) {
|
default <T> T getClientAttribute(ClientType clientType, Class<T> tClass) {
|
||||||
String propertyName = getPropertyName();
|
String propertyName = getPropertyName();
|
||||||
Object nonApplicableValue = getNonApplicableValue();
|
Object nonApplicableValue = getNonApplicableValue();
|
||||||
|
|
||||||
|
@ -103,32 +102,25 @@ interface TypedClientAttribute {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is read-only. If yes, then we just directly delegate to return stuff from the clientType rather than from client
|
return clientType.getTypeValue(propertyName, tClass);
|
||||||
if (clientType.isReadOnly(propertyName)) {
|
|
||||||
return clientType.getDefaultValue(propertyName, tClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delegate to clientGetter
|
|
||||||
return clientGetter.get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
String propertyName = getPropertyName();
|
String propertyName = getPropertyName();
|
||||||
Object nonApplicableValue = getNonApplicableValue();
|
// If feature is set as non-applicable, return directly
|
||||||
// Check if clientType supports the feature. If not, return directly
|
if (!clientType.isApplicable(propertyName)) {
|
||||||
if (!clientType.isApplicable(propertyName) && !Objects.equals(nonApplicableValue, newValue)) {
|
if(!Objects.equals(getNonApplicableValue(), newValue)) {
|
||||||
logger.warnf("Property %s is not-applicable to client type %s and can not be modified.", propertyName, clientType.getName());
|
logger.warnf("Property %s is not-applicable to client type %s and can not be modified.", propertyName, clientType.getName());
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is read-only. If yes and there is an attempt to change some stuff, 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.
|
||||||
if (clientType.isReadOnly(propertyName)) {
|
T oldVal = clientType.getTypeValue(propertyName, tClass);
|
||||||
T oldVal = clientType.getDefaultValue(propertyName, tClass);
|
if (!ObjectUtil.isEqualOrBothNull(oldVal, newValue)) {
|
||||||
if (!ObjectUtil.isEqualOrBothNull(oldVal, newValue)) {
|
throw new ClientTypeException(
|
||||||
throw new ClientTypeException(
|
"Property " + propertyName + " is read-only due to client type " + clientType.getName(),
|
||||||
"Property " + propertyName + " is read-only due to client type " + clientType.getName(),
|
propertyName);
|
||||||
propertyName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delegate to clientSetter
|
// Delegate to clientSetter
|
|
@ -23,7 +23,8 @@ 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.Map;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
@ -43,34 +44,31 @@ public class DefaultClientType implements ClientType {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isApplicable(String optionName) {
|
public boolean isApplicable(String optionName) {
|
||||||
ClientTypeRepresentation.PropertyConfig cfg = clientType.getConfig().get(optionName);
|
|
||||||
|
|
||||||
// Each property is applicable by default if not configured for the particular client type
|
// Each property is applicable by default if not configured for the particular client type
|
||||||
return (cfg != null && cfg.getApplicable() != null) ? cfg.getApplicable() : true;
|
return getConfiguration(optionName)
|
||||||
|
.map(ClientTypeRepresentation.PropertyConfig::getApplicable)
|
||||||
|
.orElse(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isReadOnly(String optionName) {
|
public <T> T getTypeValue(String optionName, Class<T> optionType) {
|
||||||
ClientTypeRepresentation.PropertyConfig cfg = clientType.getConfig().get(optionName);
|
|
||||||
|
|
||||||
// Each property is writable by default if not configured for the particular type
|
return getConfiguration(optionName)
|
||||||
return (cfg != null && cfg.getReadOnly() != null) ? cfg.getReadOnly() : false;
|
.map(ClientTypeRepresentation.PropertyConfig::getValue)
|
||||||
|
.map(optionType::cast).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T getDefaultValue(String optionName, Class<T> optionType) {
|
public Set<String> getOptionNames() {
|
||||||
ClientTypeRepresentation.PropertyConfig cfg = clientType.getConfig().get(optionName);
|
return clientType.getConfig().keySet();
|
||||||
|
|
||||||
return (cfg != null && cfg.getDefaultValue() != null) ? optionType.cast(cfg.getDefaultValue()) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Map<String, ClientTypeRepresentation.PropertyConfig> getConfiguration() {
|
|
||||||
return clientType.getConfig();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@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));
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -50,8 +50,8 @@ public class DefaultClientTypeProvider implements ClientTypeProvider {
|
||||||
throw new ClientTypeException("Invalid configuration of 'applicable' property on client type");
|
throw new ClientTypeException("Invalid configuration of 'applicable' property on client type");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not supported to set read-only or default-value for properties, which are not applicable for the particular client
|
// Not supported to set value for properties, which are not applicable for the particular client
|
||||||
if (!propConfig.getApplicable() && (propConfig.getReadOnly() != null || propConfig.getDefaultValue() != null)) {
|
if (!propConfig.getApplicable() && propConfig.getValue() != null) {
|
||||||
logger.errorf("Property '%s' is not applicable and so should not have read-only or default-value set for client type '%s'", propertyName, clientType.getName());
|
logger.errorf("Property '%s' is not applicable and so should not have read-only or default-value set for client type '%s'", propertyName, clientType.getName());
|
||||||
throw new ClientTypeException("Invalid configuration of property on client type");
|
throw new ClientTypeException("Invalid configuration of property on client type");
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,53 @@
|
||||||
"config": {
|
"config": {
|
||||||
"standardFlowEnabled": {
|
"standardFlowEnabled": {
|
||||||
"applicable": true,
|
"applicable": true,
|
||||||
"default-value": true,
|
"value": true
|
||||||
"read-only": true
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "oidc",
|
||||||
|
"provider": "default",
|
||||||
|
"config": {
|
||||||
|
"authorizationServicesEnabled": {
|
||||||
|
"applicable": true
|
||||||
|
},
|
||||||
|
"consentRequired": {
|
||||||
|
"applicable": true
|
||||||
|
},
|
||||||
|
"directAccessGrantsEnabled": {
|
||||||
|
"applicable": true
|
||||||
|
},
|
||||||
|
"frontchannelLogout": {
|
||||||
|
"applicable": true
|
||||||
|
},
|
||||||
|
"fullScopeAllowed": {
|
||||||
|
"applicable": true
|
||||||
|
},
|
||||||
|
"implicitFlowEnabled": {
|
||||||
|
"applicable": true
|
||||||
|
},
|
||||||
|
"nodeReRegistrationTimeout": {
|
||||||
|
"applicable": true
|
||||||
|
},
|
||||||
|
"oauth2.device.authorization.grant.enabled": {
|
||||||
|
"applicable": true
|
||||||
|
},
|
||||||
|
"oidc.ciba.grant.enabled": {
|
||||||
|
"applicable": true
|
||||||
|
},
|
||||||
|
"protocol": {
|
||||||
|
"applicable": true,
|
||||||
|
"value": "openid-connect"
|
||||||
|
},
|
||||||
|
"publicClient": {
|
||||||
|
"applicable": true
|
||||||
|
},
|
||||||
|
"serviceAccountsEnabled": {
|
||||||
|
"applicable": true
|
||||||
|
},
|
||||||
|
"standardFlowEnabled": {
|
||||||
|
"applicable": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -26,8 +71,7 @@
|
||||||
},
|
},
|
||||||
"consentRequired": {
|
"consentRequired": {
|
||||||
"applicable": true,
|
"applicable": true,
|
||||||
"default-value": false,
|
"value": false
|
||||||
"read-only": true
|
|
||||||
},
|
},
|
||||||
"directAccessGrantsEnabled": {
|
"directAccessGrantsEnabled": {
|
||||||
"applicable": false
|
"applicable": false
|
||||||
|
@ -55,21 +99,18 @@
|
||||||
},
|
},
|
||||||
"protocol": {
|
"protocol": {
|
||||||
"applicable": true,
|
"applicable": true,
|
||||||
"default-value": "openid-connect",
|
"value": "openid-connect"
|
||||||
"read-only": true
|
|
||||||
},
|
},
|
||||||
"publicClient": {
|
"publicClient": {
|
||||||
"applicable": true,
|
"applicable": true,
|
||||||
"default-value": false,
|
"value": false
|
||||||
"read-only": true
|
|
||||||
},
|
},
|
||||||
"redirectUris": {
|
"redirectUris": {
|
||||||
"applicable": false
|
"applicable": false
|
||||||
},
|
},
|
||||||
"serviceAccountsEnabled": {
|
"serviceAccountsEnabled": {
|
||||||
"applicable": true,
|
"applicable": true,
|
||||||
"default-value": true,
|
"value": true
|
||||||
"read-only": true
|
|
||||||
},
|
},
|
||||||
"standardFlowEnabled": {
|
"standardFlowEnabled": {
|
||||||
"applicable": false
|
"applicable": false
|
||||||
|
|
|
@ -22,7 +22,6 @@ import jakarta.ws.rs.BadRequestException;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.client.clienttype.ClientTypeManager;
|
import org.keycloak.client.clienttype.ClientTypeManager;
|
||||||
import org.keycloak.common.util.ObjectUtil;
|
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
@ -45,7 +44,9 @@ import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
import static org.hamcrest.Matchers.everyItem;
|
||||||
|
import static org.hamcrest.Matchers.hasItems;
|
||||||
|
import static org.hamcrest.Matchers.in;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
import static org.keycloak.common.Profile.Feature.CLIENT_TYPES;
|
import static org.keycloak.common.Profile.Feature.CLIENT_TYPES;
|
||||||
|
@ -154,8 +155,8 @@ public class ClientTypesTest extends AbstractTestRealmKeycloakTest {
|
||||||
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);
|
||||||
assertThat(
|
assertThat(
|
||||||
List.of(errorRepresentation.getParams()),
|
List.of(items),
|
||||||
containsInAnyOrder(items));
|
everyItem(in(errorRepresentation.getParams())));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -164,10 +165,9 @@ public class ClientTypesTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
assertEquals(0, clientTypes.getRealmClientTypes().size());
|
assertEquals(0, clientTypes.getRealmClientTypes().size());
|
||||||
|
|
||||||
List<String> globalClientTypeNames = clientTypes.getGlobalClientTypes().stream()
|
List<ClientTypeRepresentation> globalClientTypeNames = clientTypes.getGlobalClientTypes().stream()
|
||||||
.map(ClientTypeRepresentation::getName)
|
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
Assert.assertNames(globalClientTypeNames, "sla", "service-account");
|
assertNames(globalClientTypeNames, "sla", "service-account");
|
||||||
|
|
||||||
ClientTypeRepresentation serviceAccountType = clientTypes.getGlobalClientTypes().stream()
|
ClientTypeRepresentation serviceAccountType = clientTypes.getGlobalClientTypes().stream()
|
||||||
.filter(clientType -> "service-account".equals(clientType.getName()))
|
.filter(clientType -> "service-account".equals(clientType.getName()))
|
||||||
|
@ -176,16 +176,15 @@ public class ClientTypesTest extends AbstractTestRealmKeycloakTest {
|
||||||
assertEquals("default", serviceAccountType.getProvider());
|
assertEquals("default", serviceAccountType.getProvider());
|
||||||
|
|
||||||
ClientTypeRepresentation.PropertyConfig cfg = serviceAccountType.getConfig().get("standardFlowEnabled");
|
ClientTypeRepresentation.PropertyConfig cfg = serviceAccountType.getConfig().get("standardFlowEnabled");
|
||||||
assertPropertyConfig("standardFlowEnabled", cfg, false, null, null);
|
assertPropertyConfig("standardFlowEnabled", cfg, false, null);
|
||||||
|
|
||||||
cfg = serviceAccountType.getConfig().get("serviceAccountsEnabled");
|
cfg = serviceAccountType.getConfig().get("serviceAccountsEnabled");
|
||||||
assertPropertyConfig("serviceAccountsEnabled", cfg, true, true, true);
|
assertPropertyConfig("serviceAccountsEnabled", cfg, true, true);
|
||||||
|
|
||||||
cfg = serviceAccountType.getConfig().get("tosUri");
|
cfg = serviceAccountType.getConfig().get("tosUri");
|
||||||
assertPropertyConfig("tosUri", cfg, false, null, null);
|
assertPropertyConfig("tosUri", cfg, false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testClientTypesAdminRestAPI_realmTypes() {
|
public void testClientTypesAdminRestAPI_realmTypes() {
|
||||||
ClientTypesRepresentation clientTypes = testRealm().clientTypes().getClientTypes();
|
ClientTypesRepresentation clientTypes = testRealm().clientTypes().getClientTypes();
|
||||||
|
@ -218,8 +217,7 @@ public class ClientTypesTest extends AbstractTestRealmKeycloakTest {
|
||||||
try {
|
try {
|
||||||
ClientTypeRepresentation.PropertyConfig cfg = clientType.getConfig().get("standardFlowEnabled");
|
ClientTypeRepresentation.PropertyConfig cfg = clientType.getConfig().get("standardFlowEnabled");
|
||||||
cfg.setApplicable(false);
|
cfg.setApplicable(false);
|
||||||
cfg.setReadOnly(true);
|
cfg.setValue(true);
|
||||||
cfg.setDefaultValue(true);
|
|
||||||
testRealm().clientTypes().updateClientTypes(clientTypes);
|
testRealm().clientTypes().updateClientTypes(clientTypes);
|
||||||
Assert.fail("Not expected to update client types");
|
Assert.fail("Not expected to update client types");
|
||||||
} catch (BadRequestException bre) {
|
} catch (BadRequestException bre) {
|
||||||
|
@ -278,14 +276,13 @@ public class ClientTypesTest extends AbstractTestRealmKeycloakTest {
|
||||||
List<String> names = clientTypes.stream()
|
List<String> names = clientTypes.stream()
|
||||||
.map(ClientTypeRepresentation::getName)
|
.map(ClientTypeRepresentation::getName)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
Assert.assertNames(names, expectedNames);
|
assertThat(names, hasItems(expectedNames));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void assertPropertyConfig(String propertyName, ClientTypeRepresentation.PropertyConfig cfg, Boolean expectedApplicable, Boolean expectedReadOnly, Object expectedDefaultValue) {
|
private void assertPropertyConfig(String propertyName, ClientTypeRepresentation.PropertyConfig cfg, Boolean expectedApplicable, Object expectedValue) {
|
||||||
assertThat("'applicable' for property " + propertyName + " not equal", ObjectUtil.isEqualOrBothNull(expectedApplicable, cfg.getApplicable()));
|
assertEquals("'applicable' for property " + propertyName + " not equal", expectedApplicable, cfg.getApplicable());
|
||||||
assertThat("'read-only' for property " + propertyName + " not equal", ObjectUtil.isEqualOrBothNull(expectedReadOnly, cfg.getReadOnly()));
|
assertEquals("'value' for property " + propertyName + " not equal", expectedValue, cfg.getValue());
|
||||||
assertThat("'default-value' ;for property " + propertyName + " not equal", ObjectUtil.isEqualOrBothNull(expectedDefaultValue, cfg.getDefaultValue()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ClientRepresentation createClientWithType(String clientId, String clientType) {
|
private ClientRepresentation createClientWithType(String clientId, String clientType) {
|
||||||
|
|
Loading…
Reference in a new issue