diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java index fdb8b3e2413..5aca8760357 100755 --- a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java @@ -31,6 +31,7 @@ public class ClientRepresentation { protected String clientId; protected String name; protected String description; + protected String type; protected String rootUrl; protected String adminUrl; protected String baseUrl; @@ -105,6 +106,14 @@ public class ClientRepresentation { this.description = description; } + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + public String getClientId() { return clientId; } diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientTypeRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientTypeRepresentation.java index 2e6e01f4215..10401ca5415 100644 --- a/core/src/main/java/org/keycloak/representations/idm/ClientTypeRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ClientTypeRepresentation.java @@ -18,7 +18,6 @@ package org.keycloak.representations.idm; -import java.util.HashMap; import java.util.Map; import com.fasterxml.jackson.annotation.JsonProperty; @@ -61,17 +60,6 @@ public class ClientTypeRepresentation { this.config = config; } - @JsonProperty("referenced-properties") - protected Map referencedProperties = new HashMap<>(); - - public Map getReferencedProperties() { - return referencedProperties; - } - - public void setReferencedProperties(Map referencedProperties) { - this.referencedProperties = referencedProperties; - } - public static class PropertyConfig { @JsonProperty("applicable") diff --git a/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/ClientTypesResource.java b/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/ClientTypesResource.java new file mode 100644 index 00000000000..a8deba8d3e1 --- /dev/null +++ b/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/ClientTypesResource.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.keycloak.admin.client.resource; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.annotations.cache.NoCache; +import org.keycloak.representations.idm.ClientTypesRepresentation; + +/** + * @author Marek Posolda + */ +public interface ClientTypesResource { + + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + ClientTypesRepresentation getClientTypes(); + + + /** + * Update client types in the realm. The "global-client-types" field of client types is ignored as it is not possible to update global types + * + * @param clientTypes + */ + @PUT + @Consumes(MediaType.APPLICATION_JSON) + void updateClientTypes(final ClientTypesRepresentation clientTypes); +} \ No newline at end of file diff --git a/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/RealmResource.java index cfecc1fa175..ffdb48b99bf 100644 --- a/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/RealmResource.java +++ b/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/RealmResource.java @@ -292,4 +292,7 @@ public interface RealmResource { @Path("organizations") OrganizationsResource organizations(); + + @Path("client-types") + ClientTypesResource clientTypes(); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index 1c652532141..700ce7c4695 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -18,7 +18,9 @@ package org.keycloak.models.cache.infinispan; import org.jboss.logging.Logger; +import org.keycloak.client.clienttype.ClientTypeManager; import org.keycloak.cluster.ClusterProvider; +import org.keycloak.common.Profile; import org.keycloak.component.ComponentModel; import org.keycloak.models.*; import org.keycloak.models.cache.CacheRealmProvider; @@ -1195,7 +1197,7 @@ public class RealmCacheSession implements CacheRealmProvider { if (invalidations.contains(delegate.getId())) return delegate; StorageId storageId = new StorageId(delegate.getId()); CachedClient cached = null; - ClientAdapter adapter = null; + ClientModel adapter = null; if (!storageId.isLocal()) { ComponentModel component = realm.getComponent(storageId.getProviderId()); @@ -1209,7 +1211,7 @@ public class RealmCacheSession implements CacheRealmProvider { } cached = new CachedClient(revision, realm, delegate); - adapter = new ClientAdapter(realm, cached, this); + adapter = toClientModel(realm, cached); long lifespan = model.getLifespan(); if (lifespan > 0) { @@ -1219,7 +1221,7 @@ public class RealmCacheSession implements CacheRealmProvider { } } else { cached = new CachedClient(revision, realm, delegate); - adapter = new ClientAdapter(realm, cached, this); + adapter = toClientModel(realm, cached); cache.addRevisioned(cached, startupRevision); } @@ -1247,9 +1249,17 @@ public class RealmCacheSession implements CacheRealmProvider { return getClientDelegate().getClientById(realm, cached.getId()); } } - ClientAdapter adapter = new ClientAdapter(realm, cached, this); + return toClientModel(realm, cached); + } - return adapter; + private ClientModel toClientModel(RealmModel realm, CachedClient cached) { + ClientAdapter client = new ClientAdapter(realm, cached, this); + + if (Profile.isFeatureEnabled(Profile.Feature.CLIENT_TYPES)) { + ClientTypeManager mgr = session.getProvider(ClientTypeManager.class); + return mgr.augmentClient(client); + } + return client; } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java index c1df1e2a9ad..50cb0d0c015 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -43,6 +43,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.hibernate.Session; import org.jboss.logging.Logger; +import org.keycloak.client.clienttype.ClientTypeManager; +import org.keycloak.common.Profile; import org.keycloak.common.util.Time; import org.keycloak.connections.jpa.util.JpaUtils; import org.keycloak.migration.MigrationModel; @@ -290,13 +292,14 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc TypedQuery query = em.createNamedQuery("getAllRedirectUrisOfEnabledClients", Map.class); query.setParameter("realm", realm.getId()); return closing(query.getResultStream() - .filter(s -> s.get("client") != null)) - .collect( - Collectors.groupingBy( - s -> new ClientAdapter(realm, em, session, (ClientEntity) s.get("client")), - Collectors.mapping(s -> (String) s.get("redirectUri"), Collectors.toSet()) - ) - ); + .filter(s -> s.get("client") != null)) + .collect( + Collectors.groupingBy( + s -> toClientModel(realm, (ClientEntity) s.get("client")), + Collectors.mapping(s -> (String) s.get("redirectUri"), Collectors.toSet()) + ) + ); + } @Override @@ -755,6 +758,8 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc @Override public ClientModel addClient(RealmModel realm, String id, String clientId) { + ClientModel resource; + if (id == null) { id = KeycloakModelUtils.generateId(); } @@ -773,7 +778,7 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc entity.setRealmId(realm.getId()); em.persist(entity); - final ClientModel resource = new ClientAdapter(realm, em, session, entity); + resource = toClientModel(realm, entity); session.getKeycloakSessionFactory().publish((ClientModel.ClientCreationEvent) () -> resource); return resource; @@ -810,9 +815,18 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc ClientEntity client = em.find(ClientEntity.class, id); // Check if client belongs to this realm if (client == null || !realm.getId().equals(client.getRealmId())) return null; - ClientAdapter adapter = new ClientAdapter(realm, em, session, client); - return adapter; + return toClientModel(realm, client); + } + private ClientModel toClientModel(RealmModel realm, ClientEntity client) { + ClientAdapter adapter = new ClientAdapter(realm, em, session, client); + + if (Profile.isFeatureEnabled(Profile.Feature.CLIENT_TYPES)) { + ClientTypeManager mgr = session.getProvider(ClientTypeManager.class); + return mgr.augmentClient(adapter); + } else { + return adapter; + } } @Override diff --git a/server-spi-private/src/main/java/org/keycloak/client/clienttype/ClientTypeSpi.java b/server-spi-private/src/main/java/org/keycloak/client/clienttype/ClientTypeSpi.java index 4593eb1fdf1..2cec8cb9519 100644 --- a/server-spi-private/src/main/java/org/keycloak/client/clienttype/ClientTypeSpi.java +++ b/server-spi-private/src/main/java/org/keycloak/client/clienttype/ClientTypeSpi.java @@ -34,7 +34,7 @@ public class ClientTypeSpi implements Spi { @Override public String getName() { - return "client-type"; + return "clientType"; } @Override diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 2aef4765e68..b82b7bd1129 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -119,6 +119,11 @@ public class ModelToRepresentation { REALM_EXCLUDED_ATTRIBUTES.add("firstBrokerLoginFlowId"); } + public static Set CLIENT_EXCLUDED_ATTRIBUTES = new HashSet<>(); + static { + CLIENT_EXCLUDED_ATTRIBUTES.add(ClientModel.TYPE); + } + private static final Logger LOG = Logger.getLogger(ModelToRepresentation.class); public static String buildGroupPath(GroupModel group) { @@ -536,6 +541,20 @@ public class ModelToRepresentation { return a; } + + public static Map stripClientAttributesIncludedAsFields(Map attributes) { + Map a = new HashMap<>(); + + for (Map.Entry e : attributes.entrySet()) { + if (CLIENT_EXCLUDED_ATTRIBUTES.contains(e.getKey())) { + continue; + } + a.put(e.getKey(), e.getValue()); + } + + return a; + } + public static void exportGroups(KeycloakSession session, RealmModel realm, RealmRepresentation rep) { rep.setGroups(toGroupHierarchy(session, realm, true).collect(Collectors.toList())); } @@ -686,13 +705,14 @@ public class ModelToRepresentation { rep.setClientId(clientModel.getClientId()); rep.setName(clientModel.getName()); rep.setDescription(clientModel.getDescription()); + rep.setType(clientModel.getType()); rep.setEnabled(clientModel.isEnabled()); rep.setAlwaysDisplayInConsole(clientModel.isAlwaysDisplayInConsole()); rep.setAdminUrl(clientModel.getManagementUrl()); rep.setPublicClient(clientModel.isPublicClient()); rep.setFrontchannelLogout(clientModel.isFrontchannelLogout()); rep.setProtocol(clientModel.getProtocol()); - rep.setAttributes(clientModel.getAttributes()); + rep.setAttributes(stripClientAttributesIncludedAsFields(clientModel.getAttributes())); rep.setAuthenticationFlowBindingOverrides(clientModel.getAuthenticationFlowBindingOverrides()); rep.setFullScopeAllowed(clientModel.isFullScopeAllowed()); rep.setBearerOnly(clientModel.isBearerOnly()); diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 4112506804b..5e197a3ad39 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -319,6 +319,7 @@ public class RepresentationToModel { ClientModel client = resourceRep.getId() != null ? realm.addClient(resourceRep.getId(), resourceRep.getClientId()) : realm.addClient(resourceRep.getClientId()); if (resourceRep.getName() != null) client.setName(resourceRep.getName()); if (resourceRep.getDescription() != null) client.setDescription(resourceRep.getDescription()); + if (resourceRep.getType() != null) client.setType(resourceRep.getType()); if (resourceRep.isEnabled() != null) client.setEnabled(resourceRep.isEnabled()); if (resourceRep.isAlwaysDisplayInConsole() != null) client.setAlwaysDisplayInConsole(resourceRep.isAlwaysDisplayInConsole()); client.setManagementUrl(resourceRep.getAdminUrl()); @@ -493,6 +494,7 @@ public class RepresentationToModel { if (newClientId != null) resource.setClientId(newClientId); if (rep.getName() != null) resource.setName(rep.getName()); if (rep.getDescription() != null) resource.setDescription(rep.getDescription()); + if (rep.getType() != null) resource.setType(rep.getType()); if (rep.isEnabled() != null) resource.setEnabled(rep.isEnabled()); if (rep.isAlwaysDisplayInConsole() != null) resource.setAlwaysDisplayInConsole(rep.isAlwaysDisplayInConsole()); if (rep.isBearerOnly() != null) resource.setBearerOnly(rep.isBearerOnly()); diff --git a/server-spi/src/main/java/org/keycloak/models/ClientModel.java b/server-spi/src/main/java/org/keycloak/models/ClientModel.java index 7dab1d442d6..636fb8eb122 100755 --- a/server-spi/src/main/java/org/keycloak/models/ClientModel.java +++ b/server-spi/src/main/java/org/keycloak/models/ClientModel.java @@ -39,6 +39,7 @@ public interface ClientModel extends ClientScopeModel, RoleContainerModel, Prot String LOGO_URI ="logoUri"; String POLICY_URI ="policyUri"; String TOS_URI ="tosUri"; + String TYPE = "type"; interface ClientCreationEvent extends ProviderEvent { ClientModel getCreatedClient(); @@ -105,6 +106,14 @@ public interface ClientModel extends ClientScopeModel, RoleContainerModel, Prot void setDescription(String description); + default String getType() { + return getAttribute(TYPE); + } + + default void setType(String type) { + setAttribute(TYPE, type); + } + boolean isEnabled(); void setEnabled(boolean enabled); diff --git a/services/src/main/java/org/keycloak/services/clienttype/DefaultClientTypeManager.java b/services/src/main/java/org/keycloak/services/clienttype/DefaultClientTypeManager.java index 06ebdcf35d4..a2791005ebb 100644 --- a/services/src/main/java/org/keycloak/services/clienttype/DefaultClientTypeManager.java +++ b/services/src/main/java/org/keycloak/services/clienttype/DefaultClientTypeManager.java @@ -34,6 +34,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.representations.idm.ClientTypeRepresentation; import org.keycloak.representations.idm.ClientTypesRepresentation; +import org.keycloak.services.clienttype.client.TypeAwareClientModelDelegate; import org.keycloak.util.JsonSerialization; /** @@ -66,6 +67,7 @@ public class DefaultClientTypeManager implements ClientTypeManager { try { // Skip validation here for performance reasons result = JsonSerialization.readValue(asStr, ClientTypesRepresentation.class); + result.setGlobalClientTypes(globalClientTypes); } catch (IOException ioe) { throw new ClientTypeException("Failed to deserialize client types from JSON string", ioe); } @@ -104,14 +106,12 @@ public class DefaultClientTypeManager implements ClientTypeManager { @Override public ClientModel augmentClient(ClientModel client) throws ClientTypeException { - //TODO:vibrown put the logic back in next Client Type PR - return client; - /*if (client.getType() == null) { + if (client.getType() == null) { return client; } else { ClientType clientType = getClientType(client.getRealm(), client.getType()); return new TypeAwareClientModelDelegate(clientType, () -> client); - }*/ + } } static List validateAndCastConfiguration(KeycloakSession session, List clientTypes, List globalTypes) { @@ -129,8 +129,8 @@ public class DefaultClientTypeManager implements ClientTypeManager { private static ClientTypeRepresentation validateAndCastConfiguration(KeycloakSession session, ClientTypeRepresentation clientType, Set currentNames) { ClientTypeProvider clientTypeProvider = session.getProvider(ClientTypeProvider.class, clientType.getProvider()); if (clientTypeProvider == null) { - logger.errorf("Did not found client type provider '%s' for the client type '%s'", clientType.getProvider(), clientType.getName()); - throw new ClientTypeException("Did not found client type provider"); + logger.errorf("Did not find client type provider '%s' for the client type '%s'", clientType.getProvider(), clientType.getName()); + throw new ClientTypeException("Did not find client type provider"); } // Validate name is not duplicated diff --git a/services/src/main/java/org/keycloak/services/clienttype/client/TypeAwareClientModelDelegate.java b/services/src/main/java/org/keycloak/services/clienttype/client/TypeAwareClientModelDelegate.java new file mode 100644 index 00000000000..aa76430d43d --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clienttype/client/TypeAwareClientModelDelegate.java @@ -0,0 +1,91 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.keycloak.services.clienttype.client; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.keycloak.common.util.ObjectUtil; +import org.keycloak.models.ClientModel; +import org.keycloak.models.delegate.ClientModelLazyDelegate; +import org.keycloak.client.clienttype.ClientType; +import org.keycloak.client.clienttype.ClientTypeException; + +/** + * Delegates to client-type and underlying delegate + * + * @author Marek Posolda + */ +public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate { + + private final ClientType clientType; + + public TypeAwareClientModelDelegate(ClientType clientType, Supplier clientModelSupplier) { + super(clientModelSupplier); + + if (clientType == null) { + throw new IllegalArgumentException("Null client type not supported for client " + getClientId()); + } + this.clientType = clientType; + } + + @Override + public boolean isStandardFlowEnabled() { + return getBooleanProperty("standardFlowEnabled", super::isStandardFlowEnabled); + } + + @Override + public void setStandardFlowEnabled(boolean standardFlowEnabled) { + setBooleanProperty("standardFlowEnabled", standardFlowEnabled, super::setStandardFlowEnabled); + } + + + protected boolean getBooleanProperty(String propertyName, Supplier clientGetter) { + // Check if clientType supports the feature. If not, simply return false + if (!clientType.isApplicable(propertyName)) { + return false; + } + + // Check if this is read-only. If yes, then we just directly delegate to return stuff from the clientType rather than from client + if (clientType.isReadOnly(propertyName)) { + return clientType.getDefaultValue(propertyName, Boolean.class); + } + + // Delegate to clientGetter + return clientGetter.get(); + } + + protected void setBooleanProperty(String propertyName, Boolean newValue, Consumer clientSetter) { + // Check if clientType supports the feature. If not, return directly + if (!clientType.isApplicable(propertyName)) { + return; + } + + // Check if this is read-only. If yes and there is an attempt to change some stuff, then throw an exception + if (clientType.isReadOnly(propertyName)) { + Boolean oldVal = clientType.getDefaultValue(propertyName, Boolean.class); + if (!ObjectUtil.isEqualOrBothNull(oldVal, newValue)) { + throw new ClientTypeException("Property " + propertyName + " of client " + getClientId() + " is read-only due to client type " + clientType.getName()); + } + } + + // Call clientSetter + clientSetter.accept(newValue); + } +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/services/clienttype/impl/DefaultClientType.java b/services/src/main/java/org/keycloak/services/clienttype/impl/DefaultClientType.java index ed18395143a..85a7c8765c6 100644 --- a/services/src/main/java/org/keycloak/services/clienttype/impl/DefaultClientType.java +++ b/services/src/main/java/org/keycloak/services/clienttype/impl/DefaultClientType.java @@ -20,11 +20,9 @@ package org.keycloak.services.clienttype.impl; import java.beans.PropertyDescriptor; import java.lang.reflect.Method; -import java.lang.reflect.Type; import java.util.HashMap; import java.util.Map; -import com.fasterxml.jackson.databind.JavaType; import org.jboss.logging.Logger; import org.keycloak.common.util.ObjectUtil; import org.keycloak.models.ClientModel; @@ -34,7 +32,6 @@ import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientTypeRepresentation; import org.keycloak.client.clienttype.ClientType; import org.keycloak.client.clienttype.ClientTypeException; -import org.keycloak.util.JsonSerialization; /** * @author Marek Posolda @@ -43,9 +40,6 @@ public class DefaultClientType implements ClientType { private static final Logger logger = Logger.getLogger(DefaultClientType.class); - // Will be used as reference in JSON. Probably just temporary solution - private static final String REFERENCE_PREFIX = "ref::"; - private final KeycloakSession session; private final ClientTypeRepresentation clientType; @@ -93,40 +87,9 @@ public class DefaultClientType implements ClientType { if (propertyConfig.getDefaultValue() != null) { if (clientRepresentationProperties.containsKey(property.getKey())) { // Java property on client representation + Method setter = clientRepresentationProperties.get(property.getKey()).getWriteMethod(); try { - PropertyDescriptor propertyDescriptor = clientRepresentationProperties.get(property.getKey()); - Method setter = propertyDescriptor.getWriteMethod(); - Object defaultVal = propertyConfig.getDefaultValue(); - if (defaultVal instanceof String && defaultVal.toString().startsWith(REFERENCE_PREFIX)) { - // TODO:client-types re-verify or remove support for "ref::" entirely from the codebase - throw new UnsupportedOperationException("Not supported to use ref:: references"); - // Reference. We need to found referred value and call the setter with it -// String referredPropertyName = defaultVal.toString().substring(REFERENCE_PREFIX.length()); -// Object referredPropertyVal = clientType.getReferencedProperties().get(referredPropertyName); -// if (referredPropertyVal == null) { -// logger.warnf("Reference '%s' not found used in property '%s' of client type '%s'", defaultVal.toString(), property.getKey(), clientType.getName()); -// throw new ClientTypeException("Cannot set property on client"); -// } -// -// // Generic collections -// Type genericType = setter.getGenericParameterTypes()[0]; -// JavaType jacksonType = JsonSerialization.mapper.constructType(genericType); -// Object converted = JsonSerialization.mapper.convertValue(referredPropertyVal, jacksonType); -// -// setter.invoke(createdClient, converted); - } else { - Type genericType = setter.getGenericParameterTypes()[0]; - - Object converted; - if (!defaultVal.getClass().equals(genericType)) { - JavaType jacksonType = JsonSerialization.mapper.constructType(genericType); - converted = JsonSerialization.mapper.convertValue(defaultVal, jacksonType); - } else { - converted = defaultVal; - } - - setter.invoke(createdClient, converted); - } + setter.invoke(createdClient, propertyConfig.getDefaultValue()); } catch (Exception e) { logger.warnf("Cannot set property '%s' on client with value '%s'. Check configuration of the client type '%s'", property.getKey(), propertyConfig.getDefaultValue(), clientType.getName()); throw new ClientTypeException("Cannot set property on client", e); diff --git a/services/src/main/java/org/keycloak/services/managers/ClientManager.java b/services/src/main/java/org/keycloak/services/managers/ClientManager.java index d4e4520a007..2c499e4c622 100644 --- a/services/src/main/java/org/keycloak/services/managers/ClientManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ClientManager.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder; import org.jboss.logging.Logger; import org.keycloak.authentication.ClientAuthenticator; import org.keycloak.authentication.ClientAuthenticatorFactory; +import org.keycloak.common.Profile; import org.keycloak.common.constants.ServiceAccountConstants; import org.keycloak.common.util.Time; import org.keycloak.models.ClientModel; @@ -41,6 +42,8 @@ import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.representations.adapters.config.BaseRealmConfig; import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.client.clienttype.ClientType; +import org.keycloak.client.clienttype.ClientTypeManager; import org.keycloak.sessions.AuthenticationSessionProvider; import java.net.URI; @@ -79,6 +82,12 @@ public class ClientManager { * @return */ public static ClientModel createClient(KeycloakSession session, RealmModel realm, ClientRepresentation rep) { + if (Profile.isFeatureEnabled(Profile.Feature.CLIENT_TYPES) && rep.getType() != null) { + ClientTypeManager mgr = session.getProvider(ClientTypeManager.class); + ClientType clientType = mgr.getClientType(realm, rep.getType()); + clientType.onCreate(rep); + } + ClientModel client = RepresentationToModel.createClient(session, realm, rep); if (rep.getProtocol() != null) { @@ -164,7 +173,13 @@ public class ClientManager { user.setServiceAccountClientLink(client.getId()); } - // Add protocol mappers to retrieve clientId in access token + // Add protocol mappers to retrieve clientId in access token. Ignore this in case type is filled (protocol mappers can be explicitly specified for particular specific type) + if (!Profile.isFeatureEnabled(Profile.Feature.CLIENT_TYPES) || client.getType() == null) { + addServiceAccountProtocolMappers(client); + } + } + + private void addServiceAccountProtocolMappers(ClientModel client) { if (client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER) == null) { logger.debugf("Creating service account protocol mapper '%s' for client '%s'", ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER, client.getClientId()); ProtocolMapperModel protocolMapper = UserSessionNoteMapper.createClaimMapper(ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER, diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java index b4591dd7dd5..3662e9246e5 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java @@ -26,8 +26,12 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.NoCache; import org.keycloak.OAuthErrorException; import org.keycloak.authorization.admin.AuthorizationService; +import org.keycloak.client.clienttype.ClientType; +import org.keycloak.client.clienttype.ClientTypeException; +import org.keycloak.client.clienttype.ClientTypeManager; import org.keycloak.common.ClientConnection; import org.keycloak.common.Profile; +import org.keycloak.common.util.ObjectUtil; import org.keycloak.common.util.Time; import org.keycloak.events.Errors; import org.keycloak.events.admin.OperationType; @@ -148,6 +152,17 @@ public class ClientResource { session.setAttribute(ClientSecretConstants.CLIENT_SECRET_ROTATION_ENABLED,Boolean.FALSE); session.clientPolicy().triggerOnEvent(new AdminClientUpdateContext(rep, client, auth.adminAuth())); + if (Profile.isFeatureEnabled(Profile.Feature.CLIENT_TYPES)) { + if (!ObjectUtil.isEqualOrBothNull(rep.getType(), client.getType())) { + throw new ClientTypeException("Not supported to change client type"); + } + if (rep.getType() != null) { + ClientTypeManager mgr = session.getProvider(ClientTypeManager.class); + ClientType clientType = mgr.getClientType(realm, rep.getType()); + clientType.onUpdate(client, rep); + } + } + updateClientFromRep(rep, client, session); ValidationUtil.validateClient(session, client, false, r -> { @@ -170,6 +185,8 @@ public class ClientResource { return Response.noContent().build(); } catch (ModelDuplicateException e) { throw ErrorResponse.exists("Client already exists"); + } catch (ClientTypeException cte) { + throw ErrorResponse.error(cte.getMessage(), Response.Status.BAD_REQUEST); } catch (ClientPolicyException cpe) { throw new ErrorResponseException(cpe.getError(), cpe.getErrorDetail(), Response.Status.BAD_REQUEST); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientTypesResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientTypesResource.java new file mode 100644 index 00000000000..3b5b6fc92fe --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientTypesResource.java @@ -0,0 +1,97 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.keycloak.services.resources.admin; + +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.NoCache; +import org.keycloak.models.RealmModel; +import org.keycloak.representations.idm.ClientTypesRepresentation; +import org.keycloak.services.ErrorResponse; +import org.keycloak.client.clienttype.ClientTypeException; +import org.keycloak.client.clienttype.ClientTypeManager; +import org.keycloak.services.resources.KeycloakOpenAPI; +import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; + +/** + * @author Marek Posolda + */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") +public class ClientTypesResource { + protected static final Logger logger = Logger.getLogger(ClientTypesResource.class); + + protected final ClientTypeManager manager; + protected final RealmModel realm; + + private final AdminPermissionEvaluator auth; + + public ClientTypesResource(ClientTypeManager manager, RealmModel realm, AdminPermissionEvaluator auth) { + this.manager = manager; + this.auth = auth; + this.realm = realm; + } + + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation(summary = "List all client types available in the current realm", + description = "This endpoint returns a list of both global and realm level client types and the attributes they set" + ) + public ClientTypesRepresentation getClientTypes() { + auth.realm().requireViewRealm(); + + try { + return manager.getClientTypes(realm); + } catch (ClientTypeException e) { + logger.error(e.getMessage(), e); + throw new BadRequestException(ErrorResponse.error(e.getMessage(), Response.Status.BAD_REQUEST)); + } + } + + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation(summary = "Update a client type", + description = "This endpoint allows you to update a realm level client type" + ) + @APIResponse(responseCode = "204", description = "No Content") + public Response updateClientTypes(final ClientTypesRepresentation clientTypes) { + auth.realm().requireManageRealm(); + + try { + manager.updateClientTypes(realm, clientTypes); + } catch (ClientTypeException e) { + logger.error(e.getMessage(), e); + throw ErrorResponse.error(e.getMessage(), Response.Status.BAD_REQUEST); + } + return Response.noContent().build(); + } +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 5cc70a3ecf4..c061612968d 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -63,6 +63,7 @@ import org.keycloak.Config; import org.keycloak.KeyPairVerifier; import org.keycloak.authentication.CredentialRegistrator; import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.client.clienttype.ClientTypeManager; import org.keycloak.common.ClientConnection; import org.keycloak.common.Profile; import org.keycloak.common.VerificationException; @@ -1228,4 +1229,11 @@ public class RealmAdminResource { ProfileHelper.requireFeature(Profile.Feature.CLIENT_POLICIES); return new ClientProfilesResource(session, auth); } + + @Path("client-types") + public ClientTypesResource getClientTypesResource() { + ProfileHelper.requireFeature(Profile.Feature.CLIENT_TYPES); + return new ClientTypesResource(session.getProvider(ClientTypeManager.class), realm, auth); + } + } diff --git a/services/src/main/resources/keycloak-default-client-types.json b/services/src/main/resources/keycloak-default-client-types.json index 38e4d7d7ada..0cbd2991825 100644 --- a/services/src/main/resources/keycloak-default-client-types.json +++ b/services/src/main/resources/keycloak-default-client-types.json @@ -61,53 +61,6 @@ "read-only": true, "default-value": true }, - "protocolMappers": { - "applicable": true, - "read-only": true, - "default-value": [ - { - "name" : "Client IP Address", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usersessionmodel-note-mapper", - "consentRequired" : false, - "config" : { - "user.session.note" : "clientAddress", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "clientAddress", - "jsonType.label" : "String" - } - }, - { - "name" : "Client Host", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usersessionmodel-note-mapper", - "consentRequired" : false, - "config" : { - "user.session.note" : "clientHost", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "clientHost", - "jsonType.label" : "String" - } - } - ] - }, - "webOrigins": { - "applicable": true, - "read-only": true, - "default-value": [ "https://foo", "https://bar"] - }, - "defaultClientScopes": { - "applicable": true, - "read-only": true, - "default-value": [ "address", "offline_access"] - }, - "optionalClientScopes": { - "applicable": true, - "read-only": true, - "default-value": [ "profile" ] - }, "logoUri": { "applicable": false }, diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientTypesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientTypesTest.java new file mode 100644 index 00000000000..539a960f6a3 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientTypesTest.java @@ -0,0 +1,289 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.keycloak.testsuite.client; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; + +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.core.Response; + +import org.junit.Test; +import org.keycloak.common.util.ObjectUtil; +import org.keycloak.models.ClientModel; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientTypeRepresentation; + +import org.keycloak.representations.idm.ClientTypesRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.client.clienttype.ClientTypeManager; +import org.keycloak.services.clienttype.impl.DefaultClientTypeProviderFactory; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.annotation.DisableFeature; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; +import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected; +import org.keycloak.testsuite.util.ClientBuilder; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.fail; +import static org.keycloak.common.Profile.Feature.CLIENT_TYPES; + +/** + * @author Marek Posolda + */ +@EnableFeature(value = CLIENT_TYPES) +public class ClientTypesTest extends AbstractTestRealmKeycloakTest { + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + } + + @Test + public void testFeatureWorksWhenEnabled() { + checkIfFeatureWorks(true); + } + + @Test + @UncaughtServerErrorExpected + @DisableFeature(value = CLIENT_TYPES, skipRestart = true) + public void testFeatureDoesntWorkWhenDisabled() { + checkIfFeatureWorks(false); + } + + // Test create client with clientType filled. Check default properties are filled + @Test + public void testCreateClientWithClientType() { + ClientRepresentation clientRep = createClientWithType("foo", ClientTypeManager.SERVICE_ACCOUNT); + Assert.assertEquals("foo", clientRep.getClientId()); + Assert.assertEquals(ClientTypeManager.SERVICE_ACCOUNT, clientRep.getType()); + Assert.assertEquals(OIDCLoginProtocol.LOGIN_PROTOCOL, clientRep.getProtocol()); + Assert.assertFalse(clientRep.isStandardFlowEnabled()); + Assert.assertFalse(clientRep.isImplicitFlowEnabled()); + Assert.assertFalse(clientRep.isDirectAccessGrantsEnabled()); + Assert.assertTrue(clientRep.isServiceAccountsEnabled()); + Assert.assertFalse(clientRep.isPublicClient()); + Assert.assertFalse(clientRep.isBearerOnly()); + + // Check type not included as client attribute + Assert.assertFalse(clientRep.getAttributes().containsKey(ClientModel.TYPE)); + } + + @Test + public void testUpdateClientWithClientType() { + ClientRepresentation clientRep = createClientWithType("foo", ClientTypeManager.SERVICE_ACCOUNT); + + // Changing type should fail + clientRep.setType(ClientTypeManager.STANDARD); + try { + testRealm().clients().get(clientRep.getId()).update(clientRep); + Assert.fail("Not expected to update client"); + } catch (BadRequestException bre) { + // Expected + } + + // Updating read-only attribute should fail + clientRep.setType(ClientTypeManager.SERVICE_ACCOUNT); + clientRep.setServiceAccountsEnabled(false); + try { + testRealm().clients().get(clientRep.getId()).update(clientRep); + Assert.fail("Not expected to update client"); + } catch (BadRequestException bre) { + // Expected + } + + // Adding non-applicable attribute should fail + clientRep.setServiceAccountsEnabled(true); + clientRep.getAttributes().put(ClientModel.LOGO_URI, "https://foo"); + try { + testRealm().clients().get(clientRep.getId()).update(clientRep); + Assert.fail("Not expected to update client"); + } catch (BadRequestException bre) { + // Expected + } + + // Update of supported attribute should be successful + clientRep.getAttributes().remove(ClientModel.LOGO_URI); + clientRep.setRootUrl("https://foo"); + testRealm().clients().get(clientRep.getId()).update(clientRep); + } + + + @Test + public void testClientTypesAdminRestAPI_globalTypes() { + ClientTypesRepresentation clientTypes = testRealm().clientTypes().getClientTypes(); + + Assert.assertEquals(0, clientTypes.getRealmClientTypes().size()); + + List globalClientTypeNames = clientTypes.getGlobalClientTypes().stream() + .map(ClientTypeRepresentation::getName) + .collect(Collectors.toList()); + Assert.assertNames(globalClientTypeNames, "sla", "service-account"); + + ClientTypeRepresentation serviceAccountType = clientTypes.getGlobalClientTypes().stream() + .filter(clientType -> "service-account".equals(clientType.getName())) + .findFirst() + .get(); + Assert.assertEquals("default", serviceAccountType.getProvider()); + + ClientTypeRepresentation.PropertyConfig cfg = serviceAccountType.getConfig().get("standardFlowEnabled"); + assertPropertyConfig("standardFlowEnabled", cfg, true, true, false); + + cfg = serviceAccountType.getConfig().get("serviceAccountsEnabled"); + assertPropertyConfig("serviceAccountsEnabled", cfg, true, true, true); + + cfg = serviceAccountType.getConfig().get("tosUri"); + assertPropertyConfig("tosUri", cfg, false, null, null); + } + + + @Test + public void testClientTypesAdminRestAPI_realmTypes() { + ClientTypesRepresentation clientTypes = testRealm().clientTypes().getClientTypes(); + + // Test invalid provider type should fail + ClientTypeRepresentation clientType = new ClientTypeRepresentation(); + try { + clientType.setName("sla1"); + clientType.setProvider("non-existent"); + clientType.setConfig(new HashMap()); + clientTypes.setRealmClientTypes(Arrays.asList(clientType)); + testRealm().clientTypes().updateClientTypes(clientTypes); + Assert.fail("Not expected to update client types"); + } catch (BadRequestException bre) { + // Expected + } + + // Test attribute without applicable should fail + try { + clientType.setProvider(DefaultClientTypeProviderFactory.PROVIDER_ID); + ClientTypeRepresentation.PropertyConfig cfg = new ClientTypeRepresentation.PropertyConfig(); + clientType.getConfig().put("standardFlowEnabled", cfg); + testRealm().clientTypes().updateClientTypes(clientTypes); + Assert.fail("Not expected to update client types"); + } catch (BadRequestException bre) { + // Expected + } + + // Test non-applicable attribute with default-value should fail + try { + ClientTypeRepresentation.PropertyConfig cfg = clientType.getConfig().get("standardFlowEnabled"); + cfg.setApplicable(false); + cfg.setReadOnly(true); + cfg.setDefaultValue(true); + testRealm().clientTypes().updateClientTypes(clientTypes); + Assert.fail("Not expected to update client types"); + } catch (BadRequestException bre) { + // Expected + } + + // Update should be successful + ClientTypeRepresentation.PropertyConfig cfg = clientType.getConfig().get("standardFlowEnabled"); + cfg.setApplicable(true); + testRealm().clientTypes().updateClientTypes(clientTypes); + + // Test duplicate name should fail + ClientTypeRepresentation clientType2 = new ClientTypeRepresentation(); + try { + clientTypes = testRealm().clientTypes().getClientTypes(); + clientType2 = new ClientTypeRepresentation(); + clientType2.setName("sla1"); + clientType2.setProvider(DefaultClientTypeProviderFactory.PROVIDER_ID); + clientType2.setConfig(new HashMap<>()); + clientTypes.getRealmClientTypes().add(clientType2); + testRealm().clientTypes().updateClientTypes(clientTypes); + Assert.fail("Not expected to update client types"); + } catch (BadRequestException bre) { + // Expected + } + + // Also test duplicated global name should fail + try { + clientType2.setName("service-account"); + testRealm().clientTypes().updateClientTypes(clientTypes); + Assert.fail("Not expected to update client types"); + } catch (BadRequestException bre) { + // Expected + } + + // Different name should be fine + clientType2.setName("different"); + testRealm().clientTypes().updateClientTypes(clientTypes); + + // Assert updated + clientTypes = testRealm().clientTypes().getClientTypes(); + assertNames(clientTypes.getRealmClientTypes(), "sla1", "different"); + assertNames(clientTypes.getGlobalClientTypes(), "sla", "service-account"); + + // Test updating global won't update anything. Nothing will be added to globalTypes + clientType2.setName("moreDifferent"); + clientTypes.getGlobalClientTypes().add(clientType2); + testRealm().clientTypes().updateClientTypes(clientTypes); + + clientTypes = testRealm().clientTypes().getClientTypes(); + assertNames(clientTypes.getRealmClientTypes(), "sla1", "different"); + assertNames(clientTypes.getGlobalClientTypes(), "sla", "service-account"); + } + + private void assertNames(List clientTypes, String... expectedNames) { + List names = clientTypes.stream() + .map(ClientTypeRepresentation::getName) + .collect(Collectors.toList()); + Assert.assertNames(names, expectedNames); + } + + + private void assertPropertyConfig(String propertyName, ClientTypeRepresentation.PropertyConfig cfg, Boolean expectedApplicable, Boolean expectedReadOnly, Object expectedDefaultValue) { + assertThat("'applicable' for property " + propertyName + " not equal", ObjectUtil.isEqualOrBothNull(expectedApplicable, cfg.getApplicable())); + assertThat("'read-only' for property " + propertyName + " not equal", ObjectUtil.isEqualOrBothNull(expectedReadOnly, cfg.getReadOnly())); + assertThat("'default-value' ;for property " + propertyName + " not equal", ObjectUtil.isEqualOrBothNull(expectedDefaultValue, cfg.getDefaultValue())); + } + + private ClientRepresentation createClientWithType(String clientId, String clientType) { + ClientRepresentation clientRep = ClientBuilder.create() + .clientId(clientId) + .type(clientType) + .build(); + Response response = testRealm().clients().create(clientRep); + String clientUUID = ApiUtil.getCreatedId(response); + getCleanup().addClientUuid(clientUUID); + + return testRealm().clients().get(clientUUID).toRepresentation(); + } + + // Check if the feature really works + private void checkIfFeatureWorks(boolean shouldWork) { + try { + ClientTypesRepresentation clientTypes = testRealm().clientTypes().getClientTypes(); + Assert.assertTrue(clientTypes.getRealmClientTypes().isEmpty()); + if (!shouldWork) + fail("Feature is available, but at this moment should be disabled"); + + } catch (Exception e) { + if (shouldWork) { + e.printStackTrace(); + fail("Feature is not available"); + } + } + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java index 42cd2db345b..1b69bcbc673 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java @@ -63,6 +63,11 @@ public class ClientBuilder { return this; } + public ClientBuilder type(String type) { + rep.setType(type); + return this; + } + public ClientBuilder consentRequired(boolean consentRequired) { rep.setConsentRequired(consentRequired); return this;