Client type service account default type (#29037)

* Adding additional non-applicable client fields to the default service-account client type configuration.

Signed-off-by: Patrick Jennings <pajennin@redhat.com>

* Creating TypedClientAttribute which maps clientmodel fields to standard client type configurations.

Adding overrides for fields in TypeAwareClientModelDelegate required for
service-account client type.

Signed-off-by: Patrick Jennings <pajennin@redhat.com>

* Splitting client type attribute enum into 3 separate enums, representing
the top level ClientModel fields, the extended attributes through the
client_attributes table, and the composable fields on
ClientRepresentation.

Signed-off-by: Patrick Jennings <pajennin@redhat.com>

* Removing reflection use for client types.

Validation will be done in the RepresentationToModel methods that are responsible for the ClientRepresentation -> ClientModel create and update static methods.

Signed-off-by: Patrick Jennings <pajennin@redhat.com>

More updates

Signed-off-by: Patrick Jennings <pajennin@redhat.com>

* Update client utilzes type aware client property update method.

Signed-off-by: Patrick Jennings <pajennin@redhat.com>

* If user inputted representation object does not contain non-null value, try to get property value from the client. Type aware client model will return non-applicable or default value to keep fields consistent.

Signed-off-by: Patrick Jennings <pajennin@redhat.com>

* Cleaning up RepresentationToModel

Signed-off-by: Patrick Jennings <pajennin@redhat.com>

* Fixing issue when updating client secret.

Signed-off-by: Patrick Jennings <pajennin@redhat.com>

* Fixing issue where created clients would not have fullscope allowed, because getter is a boolean and so cannot be null.

Signed-off-by: Patrick Jennings <pajennin@redhat.com>

* Need to be able to clear out client attributes on update as was allowed before and causing failures in integration tests.

Signed-off-by: Patrick Jennings <pajennin@redhat.com>

* Fixing issues with redirectUri and weborigins defaults in type aware clients.

Signed-off-by: Patrick Jennings <pajennin@redhat.com>

* Need to allow client attributes the ability to clear out values during update.

Signed-off-by: Patrick Jennings <pajennin@redhat.com>

* Renaming interface based on PR feedback.

Signed-off-by: Patrick Jennings <pajennin@redhat.com>

* Shall be able to override URI sets with an empty set.

Signed-off-by: Patrick Jennings <pajennin@redhat.com>

* Comments around fields that are primitive and may cause problems determining whether to set sane default on create.

Signed-off-by: Patrick Jennings <pajennin@redhat.com>

---------

Signed-off-by: Patrick Jennings <pajennin@redhat.com>
This commit is contained in:
Patrick Jennings 2024-05-02 06:22:02 -04:00 committed by GitHub
parent 65bdf1a604
commit 64824bb77f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 620 additions and 438 deletions

View file

@ -19,7 +19,9 @@
package org.keycloak.client.clienttype;
import org.keycloak.models.ClientModel;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientTypeRepresentation;
import java.util.Map;
/**
* TODO:client-types javadocs
@ -41,10 +43,8 @@ public interface ClientType {
<T> T getDefaultValue(String optionName, Class<T> optionType);
// Augment at the client type
// Augment particular client on creation of client (TODO:client-types Should it be clientModel or clientRepresentation? Or something else?)
void onCreate(ClientRepresentation newClient) throws ClientTypeException;
Map<String, ClientTypeRepresentation.PropertyConfig> getConfiguration();
// Augment particular client on update of client (TODO:client-types Should it be clientModel or clientRepresentation? Or something else?)
void onUpdate(ClientModel currentClient, ClientRepresentation clientToUpdate) throws ClientTypeException;
// Augment at the client type
ClientModel augment(ClientModel client);
}

View file

@ -23,7 +23,7 @@ import org.keycloak.models.ModelIllegalStateException;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicMarkableReference;

View file

@ -18,17 +18,23 @@
package org.keycloak.models.utils;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
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;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jboss.logging.Logger;
import org.keycloak.Config;
@ -50,8 +56,12 @@ import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.IdentityProviderFactory;
import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.client.clienttype.ClientType;
import org.keycloak.client.clienttype.ClientTypeException;
import org.keycloak.client.clienttype.ClientTypeManager;
import org.keycloak.common.Profile;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.ObjectUtil;
import org.keycloak.common.util.UriUtils;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialModel;
@ -317,18 +327,14 @@ public class RepresentationToModel {
logger.debugv("Create client: {0}", resourceRep.getClientId());
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());
if (resourceRep.isSurrogateAuthRequired() != null)
client.setSurrogateAuthRequired(resourceRep.isSurrogateAuthRequired());
if (resourceRep.getRootUrl() != null) client.setRootUrl(resourceRep.getRootUrl());
if (resourceRep.getBaseUrl() != null) client.setBaseUrl(resourceRep.getBaseUrl());
if (resourceRep.isBearerOnly() != null) client.setBearerOnly(resourceRep.isBearerOnly());
if (resourceRep.isConsentRequired() != null) client.setConsentRequired(resourceRep.isConsentRequired());
if (Profile.isFeatureEnabled(Profile.Feature.CLIENT_TYPES) && resourceRep.getType() != null) {
ClientTypeManager mgr = session.getProvider(ClientTypeManager.class);
ClientType clientType = mgr.getClientType(realm, resourceRep.getType());
client = clientType.augment(client);
}
updateClientProperties(client, resourceRep, true);
// Backwards compatibility only
if (resourceRep.isDirectGrantsOnly() != null) {
@ -337,58 +343,6 @@ public class RepresentationToModel {
client.setDirectAccessGrantsEnabled(resourceRep.isDirectGrantsOnly());
}
if (resourceRep.isStandardFlowEnabled() != null)
client.setStandardFlowEnabled(resourceRep.isStandardFlowEnabled());
if (resourceRep.isImplicitFlowEnabled() != null)
client.setImplicitFlowEnabled(resourceRep.isImplicitFlowEnabled());
if (resourceRep.isDirectAccessGrantsEnabled() != null)
client.setDirectAccessGrantsEnabled(resourceRep.isDirectAccessGrantsEnabled());
if (resourceRep.isServiceAccountsEnabled() != null)
client.setServiceAccountsEnabled(resourceRep.isServiceAccountsEnabled());
if (resourceRep.isPublicClient() != null) client.setPublicClient(resourceRep.isPublicClient());
if (resourceRep.isFrontchannelLogout() != null)
client.setFrontchannelLogout(resourceRep.isFrontchannelLogout());
// set defaults to openid-connect if no protocol specified
if (resourceRep.getProtocol() != null) {
client.setProtocol(resourceRep.getProtocol());
} else {
client.setProtocol(OIDC);
}
if (resourceRep.getNodeReRegistrationTimeout() != null) {
client.setNodeReRegistrationTimeout(resourceRep.getNodeReRegistrationTimeout());
} else {
client.setNodeReRegistrationTimeout(-1);
}
if (resourceRep.getNotBefore() != null) {
client.setNotBefore(resourceRep.getNotBefore());
}
if (resourceRep.getClientAuthenticatorType() != null) {
client.setClientAuthenticatorType(resourceRep.getClientAuthenticatorType());
} else {
client.setClientAuthenticatorType(KeycloakModelUtils.getDefaultClientAuthenticatorType());
}
// adding secret if the client isn't public nor bearer only
if (Objects.nonNull(resourceRep.getSecret())) {
client.setSecret(resourceRep.getSecret());
} else {
if (client.isPublicClient() || client.isBearerOnly()) {
client.setSecret(null);
} else {
KeycloakModelUtils.generateSecret(client);
}
}
if (resourceRep.getAttributes() != null) {
for (Map.Entry<String, String> entry : resourceRep.getAttributes().entrySet()) {
client.setAttribute(entry.getKey(), entry.getValue());
}
}
if ("saml".equals(resourceRep.getProtocol())
&& (resourceRep.getAttributes() == null
|| !resourceRep.getAttributes().containsKey("saml.artifact.binding.identifier"))) {
@ -413,35 +367,6 @@ public class RepresentationToModel {
}
}
if (resourceRep.getRedirectUris() != null) {
for (String redirectUri : resourceRep.getRedirectUris()) {
client.addRedirectUri(redirectUri);
}
}
if (resourceRep.getWebOrigins() != null) {
for (String webOrigin : resourceRep.getWebOrigins()) {
logger.debugv("Client: {0} webOrigin: {1}", resourceRep.getClientId(), webOrigin);
client.addWebOrigin(webOrigin);
}
} else {
// add origins from redirect uris
if (resourceRep.getRedirectUris() != null) {
Set<String> origins = new HashSet<String>();
for (String redirectUri : resourceRep.getRedirectUris()) {
logger.debugv("add redirect-uri to origin: {0}", redirectUri);
if (redirectUri.startsWith("http")) {
String origin = UriUtils.getOrigin(redirectUri);
logger.debugv("adding default client origin: {0}", origin);
origins.add(origin);
}
}
if (origins.size() > 0) {
client.setWebOrigins(origins);
}
}
}
if (resourceRep.getRegisteredNodes() != null) {
for (Map.Entry<String, Integer> entry : resourceRep.getRegisteredNodes().entrySet()) {
client.registerNode(entry.getKey(), entry.getValue());
@ -457,7 +382,6 @@ public class RepresentationToModel {
}
MigrationUtils.updateProtocolMappers(client);
}
if (resourceRep.getClientTemplate() != null) {
@ -467,12 +391,6 @@ public class RepresentationToModel {
updateClientScopes(resourceRep, client);
if (resourceRep.isFullScopeAllowed() != null) {
client.setFullScopeAllowed(resourceRep.isFullScopeAllowed());
} else {
client.setFullScopeAllowed(!client.isConsentRequired());
}
client.updateClient();
resourceRep.setId(client.getId());
@ -489,44 +407,25 @@ public class RepresentationToModel {
}
public static void updateClient(ClientRepresentation rep, ClientModel resource, KeycloakSession session) {
if (Profile.isFeatureEnabled(Profile.Feature.CLIENT_TYPES)) {
if (!ObjectUtil.isEqualOrBothNull(rep.getType(), rep.getType())) {
throw new ClientTypeException("Not supported to change client type");
}
if (rep.getType() != null) {
RealmModel realm = session.getContext().getRealm();
ClientTypeManager mgr = session.getProvider(ClientTypeManager.class);
ClientType clientType = mgr.getClientType(realm, rep.getType());
resource = clientType.augment(resource);
}
}
String newClientId = rep.getClientId();
String previousClientId = resource.getClientId();
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());
if (rep.isConsentRequired() != null) resource.setConsentRequired(rep.isConsentRequired());
if (rep.isStandardFlowEnabled() != null) resource.setStandardFlowEnabled(rep.isStandardFlowEnabled());
if (rep.isImplicitFlowEnabled() != null) resource.setImplicitFlowEnabled(rep.isImplicitFlowEnabled());
if (rep.isDirectAccessGrantsEnabled() != null)
resource.setDirectAccessGrantsEnabled(rep.isDirectAccessGrantsEnabled());
if (rep.isServiceAccountsEnabled() != null) resource.setServiceAccountsEnabled(rep.isServiceAccountsEnabled());
if (rep.isPublicClient() != null) resource.setPublicClient(rep.isPublicClient());
if (rep.isFullScopeAllowed() != null) resource.setFullScopeAllowed(rep.isFullScopeAllowed());
if (rep.isFrontchannelLogout() != null) resource.setFrontchannelLogout(rep.isFrontchannelLogout());
if (rep.getRootUrl() != null) resource.setRootUrl(rep.getRootUrl());
if (rep.getAdminUrl() != null) resource.setManagementUrl(rep.getAdminUrl());
if (rep.getBaseUrl() != null) resource.setBaseUrl(rep.getBaseUrl());
if (rep.isSurrogateAuthRequired() != null) resource.setSurrogateAuthRequired(rep.isSurrogateAuthRequired());
if (rep.getNodeReRegistrationTimeout() != null)
resource.setNodeReRegistrationTimeout(rep.getNodeReRegistrationTimeout());
if (rep.getClientAuthenticatorType() != null)
resource.setClientAuthenticatorType(rep.getClientAuthenticatorType());
if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol());
if (rep.getAttributes() != null) {
for (Map.Entry<String, String> entry : rep.getAttributes().entrySet()) {
resource.setAttribute(entry.getKey(), entry.getValue());
}
}
if (rep.getAttributes() != null) {
for (Map.Entry<String, String> entry : removeEmptyString(rep.getAttributes()).entrySet()) {
resource.setAttribute(entry.getKey(), entry.getValue());
}
}
if (newClientId != null) resource.setClientId(newClientId);
updateClientProperties(resource, rep, false);
if ("saml".equals(rep.getProtocol())
&& (rep.getAttributes() == null
@ -548,46 +447,20 @@ public class RepresentationToModel {
}
}
if (rep.getNotBefore() != null) {
resource.setNotBefore(rep.getNotBefore());
}
List<String> redirectUris = rep.getRedirectUris();
if (redirectUris != null) {
resource.setRedirectUris(new HashSet<>(redirectUris));
}
List<String> webOrigins = rep.getWebOrigins();
if (webOrigins != null) {
resource.setWebOrigins(new HashSet<>(webOrigins));
}
if (rep.getRegisteredNodes() != null) {
for (Map.Entry<String, Integer> entry : rep.getRegisteredNodes().entrySet()) {
resource.registerNode(entry.getKey(), entry.getValue());
}
}
if (resource.isPublicClient() || resource.isBearerOnly()) {
resource.setSecret(null);
} else {
String currentSecret = resource.getSecret();
String newSecret = rep.getSecret();
if (newSecret == null && currentSecret == null) {
KeycloakModelUtils.generateSecret(resource);
} else if (newSecret != null) {
resource.setSecret(newSecret);
}
}
resource.updateClient();
if (!Objects.equals(newClientId, previousClientId)) {
ClientModel finalResource = resource;
ClientModel.ClientIdChangeEvent event = new ClientModel.ClientIdChangeEvent() {
@Override
public ClientModel getUpdatedClient() {
return resource;
return finalResource;
}
@Override
@ -609,6 +482,126 @@ public class RepresentationToModel {
}
}
/**
* Update client properties and process any {@link ClientTypeException} validation errors combining into one to be thrown.
*
* @param client {@link ClientModel} to update
* @param rep {@link ClientRepresentation} to apply updates.
* @param isNew Whether the client is new or not (needed because some getters cannot return null).
*/
private static void updateClientProperties(ClientModel client, ClientRepresentation rep, boolean isNew) {
List<Supplier<ClientTypeException>> clientPropertyUpdates = new LinkedList<Supplier<ClientTypeException>>() {{
/**
* Values from the ClientRepresentation take precedence.
* Then values from the ClientModel (these may be augmented from the client type configuration).
* Otherwise, we can choose some sane default.
*/
add(updatePropertyAction(client::setName, rep::getName, client::getName));
add(updatePropertyAction(client::setDescription, rep::getDescription, client::getDescription));
add(updatePropertyAction(client::setType, rep::getType, client::getType));
add(updatePropertyAction(client::setEnabled, rep::isEnabled, client::isEnabled));
add(updatePropertyAction(client::setAlwaysDisplayInConsole, rep::isAlwaysDisplayInConsole, client::isAlwaysDisplayInConsole));
add(updatePropertyAction(client::setManagementUrl, rep::getAdminUrl, client::getManagementUrl));
add(updatePropertyAction(client::setSurrogateAuthRequired, rep::isSurrogateAuthRequired, client::isSurrogateAuthRequired));
add(updatePropertyAction(client::setRootUrl, rep::getRootUrl, client::getRootUrl));
add(updatePropertyAction(client::setBaseUrl, rep::getBaseUrl, client::getBaseUrl));
add(updatePropertyAction(client::setBearerOnly, rep::isBearerOnly, client::isBearerOnly));
add(updatePropertyAction(client::setConsentRequired, rep::isConsentRequired, client::isConsentRequired));
add(updatePropertyAction(client::setStandardFlowEnabled, rep::isStandardFlowEnabled, client::isStandardFlowEnabled));
add(updatePropertyAction(client::setImplicitFlowEnabled, rep::isImplicitFlowEnabled, client::isImplicitFlowEnabled));
add(updatePropertyAction(client::setDirectAccessGrantsEnabled, rep::isDirectAccessGrantsEnabled, client::isDirectAccessGrantsEnabled));
add(updatePropertyAction(client::setServiceAccountsEnabled, rep::isServiceAccountsEnabled, client::isServiceAccountsEnabled));
add(updatePropertyAction(client::setPublicClient, rep::isPublicClient, client::isPublicClient));
add(updatePropertyAction(client::setFrontchannelLogout, rep::isFrontchannelLogout, client::isFrontchannelLogout));
add(updatePropertyAction(client::setNotBefore, rep::getNotBefore, client::getNotBefore));
// Fields with defaults if not initially provided
add(updatePropertyAction(client::setProtocol, rep::getProtocol, client::getProtocol, () -> OIDC));
add(updatePropertyAction(client::setNodeReRegistrationTimeout, rep::getNodeReRegistrationTimeout, () -> defaultNodeReRegistrationTimeout(client, isNew)));
add(updatePropertyAction(client::setClientAuthenticatorType, rep::getClientAuthenticatorType, client::getClientAuthenticatorType, KeycloakModelUtils::getDefaultClientAuthenticatorType));
add(updatePropertyAction(client::setFullScopeAllowed, rep::isFullScopeAllowed, () -> defaultFullScopeAllowed(client, isNew)));
// Client Secret
add(updatePropertyAction(client::setSecret, () -> determineNewSecret(client, rep)));
// Redirect uris / Web origins
add(updatePropertyAction(client::setRedirectUris, () -> collectionToSet(rep.getRedirectUris()), client::getRedirectUris));
add(updatePropertyAction(client::setWebOrigins, () -> collectionToSet(rep.getWebOrigins()), () -> defaultWebOrigins(client)));
}};
// Extended client attributes
if (rep.getAttributes() != null) {
for (Map.Entry<String, String> entry : rep.getAttributes().entrySet()) {
clientPropertyUpdates.add(
updatePropertyAction(val -> client.setAttribute(entry.getKey(), val), entry::getValue));
}
}
List<ClientTypeException> propertyUpdateExceptions = clientPropertyUpdates
.stream()
.map(Supplier::get)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (propertyUpdateExceptions.size() > 0) {
throw new ClientTypeException(
"Cannot change property of client as it is not allowed by the specified client type.",
propertyUpdateExceptions.stream().map(ClientTypeException::getParameters).flatMap(Stream::of).toArray());
}
}
private static Boolean defaultFullScopeAllowed(ClientModel client, boolean isNew) {
// If the client is newly created and the value is set to true, the value must be augmented through a
// client type configuration, so do not update. Yet if new and false, the field MAY be controlled through a
// client type. We can only attempt to set the sane default based on consent required.
return isNew && !client.isFullScopeAllowed() ? !client.isConsentRequired() : client.isFullScopeAllowed();
}
private static Integer defaultNodeReRegistrationTimeout(ClientModel client, boolean isNew) {
// If the client is newly created and the value is 0, the client MAY not be augmented with a client type.
if (isNew && Objects.equals(client.getNodeReRegistrationTimeout(), 0)) {
// Return the sane default.
return -1;
}
// Update client request without an overriding value or is new and augmented with a client type,
// do not attempt to update.
return client.getNodeReRegistrationTimeout();
}
private static String determineNewSecret(ClientModel client, ClientRepresentation rep) {
if (Boolean.TRUE.equals(rep.isPublicClient()) || Boolean.TRUE.equals(rep.isBearerOnly())) {
// Clear out the secret with null
return null;
}
// adding secret if the client isn't public nor bearer only
String currentSecret = client.getSecret();
String newSecret = rep.getSecret();
if (newSecret == null && currentSecret == null) {
return KeycloakModelUtils.generateSecret(client);
} else if (newSecret != null) {
return newSecret;
}
// Do not change current secret.
return currentSecret;
}
private static Set<String> defaultWebOrigins(ClientModel client) {
Set<String> webOrigins = client.getWebOrigins();
if (webOrigins != null && !webOrigins.isEmpty()) {
return webOrigins;
}
Set<String> redirectUris = client.getRedirectUris();
if (redirectUris == null || redirectUris.isEmpty()) {
return new HashSet<>();
}
return client.getRedirectUris()
.stream()
.filter(uri -> uri.startsWith("http"))
.map(UriUtils::getOrigin)
.collect(Collectors.toSet());
}
public static void updateClientProtocolMappers(ClientRepresentation rep, ClientModel resource) {
if (rep.getProtocolMappers() != null) {
@ -663,6 +656,45 @@ public class RepresentationToModel {
}
}
/**
* Create Supplier to update property, if not null.
* Captures {@link ClientTypeException} if thrown by the setter.
*
* @param modelSetter setter to call.
* @param representationGetter getter supplying the property update.
* @return {@link Supplier<T>} resulting whether a {@link ClientTypeException} was thrown.
* @param <T> Type of property.
*/
private static <T> Supplier<ClientTypeException> updateProperty(Consumer<T> modelSetter, Supplier<T> representationGetter) {
return () -> {
try {
T value = representationGetter.get();
modelSetter.accept(value);
} catch (ClientTypeException cte) {
return cte;
}
return null;
};
}
/**
* Create an update property action, passing the first non-null value supplied to the setter, in argument order.
*
* @param modelSetter {@link Consumer<T>} The setter to call.
* @param getters {@link Supplier<T>} to query for the first non-null value.
* @return {@link Supplier} with results of operation.
* @param <T> Type of property.
*/
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);
return updateProperty(modelSetter, () -> firstNonNullSupplied.findFirst().orElse(null));
}
private static String generateProtocolNameKey(String protocol, String name) {
return String.format("%s%%%s", protocol, name);
}
@ -1607,4 +1639,10 @@ public class RepresentationToModel {
return toModel(representation, authorization, client);
}
private static <T> Set<T> collectionToSet(Collection<T> collection) {
return Optional.ofNullable(collection)
.map(HashSet::new)
.orElse(null);
}
}

View file

@ -18,14 +18,15 @@
package org.keycloak.services.clienttype.client;
import java.util.function.Consumer;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
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
@ -47,45 +48,219 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate {
@Override
public boolean isStandardFlowEnabled() {
return getBooleanProperty("standardFlowEnabled", super::isStandardFlowEnabled);
return TypedClientSimpleAttribute.STANDARD_FLOW_ENABLED
.getClientAttribute(clientType, super::isStandardFlowEnabled, Boolean.class);
}
@Override
public void setStandardFlowEnabled(boolean standardFlowEnabled) {
setBooleanProperty("standardFlowEnabled", standardFlowEnabled, super::setStandardFlowEnabled);
TypedClientSimpleAttribute.STANDARD_FLOW_ENABLED
.setClientAttribute(clientType, standardFlowEnabled, super::setStandardFlowEnabled, Boolean.class);
}
protected boolean getBooleanProperty(String propertyName, Supplier<Boolean> clientGetter) {
// Check if clientType supports the feature. If not, simply return false
if (!clientType.isApplicable(propertyName)) {
return false;
@Override
public boolean isBearerOnly() {
return TypedClientSimpleAttribute.BEARER_ONLY
.getClientAttribute(clientType, super::isBearerOnly, Boolean.class);
}
// 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);
@Override
public void setBearerOnly(boolean bearerOnly) {
TypedClientSimpleAttribute.BEARER_ONLY
.setClientAttribute(clientType, bearerOnly, super::setBearerOnly, Boolean.class);
}
// Delegate to clientGetter
return clientGetter.get();
@Override
public boolean isConsentRequired() {
return TypedClientSimpleAttribute.CONSENT_REQUIRED
.getClientAttribute(clientType, super::isConsentRequired, Boolean.class);
}
protected void setBooleanProperty(String propertyName, Boolean newValue, Consumer<Boolean> clientSetter) {
// Check if clientType supports the feature. If not, return directly
if (!clientType.isApplicable(propertyName)) {
return;
@Override
public void setConsentRequired(boolean consentRequired) {
TypedClientSimpleAttribute.CONSENT_REQUIRED
.setClientAttribute(clientType, consentRequired, super::setConsentRequired, Boolean.class);
}
// 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());
@Override
public boolean isDirectAccessGrantsEnabled() {
return TypedClientSimpleAttribute.DIRECT_ACCESS_GRANTS_ENABLED
.getClientAttribute(clientType, super::isDirectAccessGrantsEnabled, Boolean.class);
}
@Override
public void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled) {
TypedClientSimpleAttribute.DIRECT_ACCESS_GRANTS_ENABLED
.setClientAttribute(clientType, directAccessGrantsEnabled, super::setDirectAccessGrantsEnabled, Boolean.class);
}
@Override
public boolean isAlwaysDisplayInConsole() {
return TypedClientSimpleAttribute.ALWAYS_DISPLAY_IN_CONSOLE
.getClientAttribute(clientType, super::isAlwaysDisplayInConsole, Boolean.class);
}
@Override
public void setAlwaysDisplayInConsole(boolean alwaysDisplayInConsole) {
TypedClientSimpleAttribute.ALWAYS_DISPLAY_IN_CONSOLE
.setClientAttribute(clientType, alwaysDisplayInConsole, super::setAlwaysDisplayInConsole, Boolean.class);
}
@Override
public boolean isFrontchannelLogout() {
return TypedClientSimpleAttribute.FRONTCHANNEL_LOGOUT
.getClientAttribute(clientType, super::isFrontchannelLogout, Boolean.class);
}
@Override
public void setFrontchannelLogout(boolean frontchannelLogout) {
TypedClientSimpleAttribute.FRONTCHANNEL_LOGOUT
.setClientAttribute(clientType, frontchannelLogout, super::setFrontchannelLogout, Boolean.class);
}
@Override
public boolean isImplicitFlowEnabled() {
return TypedClientSimpleAttribute.IMPLICIT_FLOW_ENABLED
.getClientAttribute(clientType, super::isImplicitFlowEnabled, Boolean.class);
}
@Override
public void setImplicitFlowEnabled(boolean implicitFlowEnabled) {
TypedClientSimpleAttribute.IMPLICIT_FLOW_ENABLED
.setClientAttribute(clientType, implicitFlowEnabled, super::setImplicitFlowEnabled, Boolean.class);
}
@Override
public boolean isServiceAccountsEnabled() {
return TypedClientSimpleAttribute.SERVICE_ACCOUNTS_ENABLED
.getClientAttribute(clientType, super::isServiceAccountsEnabled, Boolean.class);
}
@Override
public void setServiceAccountsEnabled(boolean flag) {
TypedClientSimpleAttribute.SERVICE_ACCOUNTS_ENABLED
.setClientAttribute(clientType, flag, super::setServiceAccountsEnabled, Boolean.class);
}
@Override
public String getProtocol() {
return TypedClientSimpleAttribute.PROTOCOL
.getClientAttribute(clientType, super::getProtocol, String.class);
}
@Override
public void setProtocol(String protocol) {
TypedClientSimpleAttribute.PROTOCOL
.setClientAttribute(clientType, protocol, super::setProtocol, String.class);
}
@Override
public boolean isPublicClient() {
return TypedClientSimpleAttribute.PUBLIC_CLIENT
.getClientAttribute(clientType, super::isPublicClient, Boolean.class);
}
@Override
public void setPublicClient(boolean flag) {
TypedClientSimpleAttribute.PUBLIC_CLIENT
.setClientAttribute(clientType, flag, super::setPublicClient, Boolean.class);
}
@Override
public Set<String> getWebOrigins() {
return TypedClientSimpleAttribute.WEB_ORIGINS
.getClientAttribute(clientType, super::getWebOrigins, Set.class);
}
@Override
public void setWebOrigins(Set<String> webOrigins) {
TypedClientSimpleAttribute.WEB_ORIGINS
.setClientAttribute(clientType, webOrigins, super::setWebOrigins, Set.class);
}
@Override
public void addWebOrigin(String webOrigin) {
TypedClientSimpleAttribute.WEB_ORIGINS
.setClientAttribute(clientType, webOrigin, super::addWebOrigin, String.class);
}
@Override
public void removeWebOrigin(String webOrigin) {
TypedClientSimpleAttribute.WEB_ORIGINS
.setClientAttribute(clientType, null, (val) -> super.removeWebOrigin(webOrigin), String.class);
}
@Override
public Set<String> getRedirectUris() {
return TypedClientSimpleAttribute.REDIRECT_URIS
.getClientAttribute(clientType, super::getRedirectUris, Set.class);
}
@Override
public void setRedirectUris(Set<String> redirectUris) {
TypedClientSimpleAttribute.REDIRECT_URIS
.setClientAttribute(clientType, redirectUris, super::setRedirectUris, Set.class);
}
@Override
public void addRedirectUri(String redirectUri) {
TypedClientSimpleAttribute.REDIRECT_URIS
.setClientAttribute(clientType, redirectUri, super::addRedirectUri, String.class);
}
@Override
public void removeRedirectUri(String redirectUri) {
TypedClientSimpleAttribute.REDIRECT_URIS
.setClientAttribute(clientType, null, (val) -> super.removeRedirectUri(redirectUri), String.class);
}
@Override
public void setAttribute(String name, String value) {
TypedClientExtendedAttribute attribute = TypedClientExtendedAttribute.getAttributesByName().get(name);
if (attribute != null) {
attribute.setClientAttribute(clientType, value, (newValue) -> super.setAttribute(name, newValue), String.class);
} else {
super.setAttribute(name, value);
}
}
// Call clientSetter
clientSetter.accept(newValue);
@Override
public void removeAttribute(String name) {
TypedClientExtendedAttribute attribute = TypedClientExtendedAttribute.getAttributesByName().get(name);
if (attribute != null) {
attribute.setClientAttribute(clientType, null, (val) -> super.removeAttribute(name), String.class);
} else {
super.removeAttribute(name);
}
}
@Override
public String getAttribute(String name) {
TypedClientExtendedAttribute attribute = TypedClientExtendedAttribute.getAttributesByName().get(name);
if (attribute != null) {
return attribute.getClientAttribute(clientType, () -> super.getAttribute(name), String.class);
} else {
return super.getAttribute(name);
}
}
@Override
public Map<String, String> getAttributes() {
// Start with attributes set on the delegate.
Map<String, String> attributes = new HashMap<>(super.getAttributes());
// Get extended client type attributes and values from the client type configuration.
Set<String> extendedClientTypeAttributes =
clientType.getConfiguration().entrySet().stream()
.map(Map.Entry::getKey)
.filter(entry -> TypedClientExtendedAttribute.getAttributesByName().containsKey(entry))
.collect(Collectors.toSet());
// Augment client type attributes on top of attributes on the delegate.
for (String entry : extendedClientTypeAttributes) {
attributes.put(entry, getAttribute(entry));
}
return attributes;
}
}

View file

@ -0,0 +1,140 @@
package org.keycloak.services.clienttype.client;
import org.jboss.logging.Logger;
import org.keycloak.client.clienttype.ClientType;
import org.keycloak.client.clienttype.ClientTypeException;
import org.keycloak.common.util.ObjectUtil;
import java.util.Arrays;
import java.util.HashMap;
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
STANDARD_FLOW_ENABLED("standardFlowEnabled", false),
BEARER_ONLY("bearerOnly", false),
CONSENT_REQUIRED("consentRequired", false),
DIRECT_ACCESS_GRANTS_ENABLED("directAccessGrantsEnabled", false),
ALWAYS_DISPLAY_IN_CONSOLE("alwaysDisplayInConsole", false),
FRONTCHANNEL_LOGOUT("frontchannelLogout", false),
IMPLICIT_FLOW_ENABLED("implicitFlowEnabled", false),
PROTOCOL("protocol", null),
PUBLIC_CLIENT("publicClient", false),
REDIRECT_URIS("redirectUris", Set.of()),
SERVICE_ACCOUNTS_ENABLED("serviceAccountsEnabled", false),
WEB_ORIGINS("webOrigins", Set.of()),
;
private final String propertyName;
private final Object nonApplicableValue;
TypedClientSimpleAttribute(String propertyName, Object nonApplicableValue) {
this.propertyName = propertyName;
this.nonApplicableValue = nonApplicableValue;
}
@Override
public String getPropertyName() {
return propertyName;
}
@Override
public Object getNonApplicableValue() {
return nonApplicableValue;
}
}
enum TypedClientExtendedAttribute implements TypedClientAttribute {
// Extended Client Type attributes defined as client attribute entities.
DEVICE_AUTHORIZATION_GRANT_ENABLED("oauth2.device.authorization.grant.enabled", "false"),
CIBA_GRANT_ENABLED("oidc.ciba.grant.enabled", "false"),
LOGIN_THEME("login_theme", null),
LOGO_URI("logoUri", null),
POLICY_URI("policyUri", null);
private static final Map<String, TypedClientExtendedAttribute> attributesByName = new HashMap<>();
static {
Arrays.stream(TypedClientExtendedAttribute.values())
.forEach(attribute -> attributesByName.put(attribute.getPropertyName(), attribute));
}
private final String propertyName;
private final Object nonApplicableValue;
TypedClientExtendedAttribute(String propertyName, Object nonApplicableValue) {
this.propertyName = propertyName;
this.nonApplicableValue = nonApplicableValue;
}
@Override
public String getPropertyName() {
return propertyName;
}
@Override
public Object getNonApplicableValue() {
return nonApplicableValue;
}
public static Map<String, TypedClientExtendedAttribute> getAttributesByName() {
return attributesByName;
}
}
interface TypedClientAttribute {
Logger logger = Logger.getLogger(TypedClientAttribute.class);
default <T> T getClientAttribute(ClientType clientType, Supplier<T> clientGetter, Class<T> tClass) {
String propertyName = getPropertyName();
Object nonApplicableValue = getNonApplicableValue();
// Check if clientType supports the feature.
if (!clientType.isApplicable(propertyName)) {
try {
return tClass.cast(nonApplicableValue);
} catch (ClassCastException e) {
logger.error("Could not apply client type property %s: %s", propertyName, e);
throw e;
}
}
// 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, tClass);
}
// Delegate to clientGetter
return clientGetter.get();
}
default <T> void setClientAttribute(ClientType clientType, T newValue, Consumer<T> clientSetter, Class<T> tClass) {
String propertyName = getPropertyName();
Object nonApplicableValue = getNonApplicableValue();
// Check if clientType supports the feature. If not, return directly
if (!clientType.isApplicable(propertyName) && !Objects.equals(nonApplicableValue, newValue)) {
logger.warnf("Property %s is not-applicable to client type %s and can not be modified.", propertyName, clientType.getName());
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)) {
T oldVal = clientType.getDefaultValue(propertyName, tClass);
if (!ObjectUtil.isEqualOrBothNull(oldVal, newValue)) {
throw new ClientTypeException(
"Property " + propertyName + " is read-only due to client type " + clientType.getName(),
propertyName);
}
}
// Delegate to clientSetter
clientSetter.accept(newValue);
}
String getPropertyName();
Object getNonApplicableValue();
}

View file

@ -18,40 +18,22 @@
package org.keycloak.services.clienttype.impl;
import jakarta.ws.rs.core.Response;
import org.jboss.logging.Logger;
import org.keycloak.client.clienttype.ClientType;
import org.keycloak.client.clienttype.ClientTypeException;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientTypeRepresentation;
import org.keycloak.services.clienttype.client.TypeAwareClientModelDelegate;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DefaultClientType implements ClientType {
private static final Logger logger = Logger.getLogger(DefaultClientType.class);
private final KeycloakSession session;
private final ClientTypeRepresentation clientType;
private final Map<String, PropertyDescriptor> clientRepresentationProperties;
public DefaultClientType(KeycloakSession session, ClientTypeRepresentation clientType, Map<String, PropertyDescriptor> clientRepresentationProperties) {
this.session = session;
public DefaultClientType(ClientTypeRepresentation clientType) {
this.clientType = clientType;
this.clientRepresentationProperties = clientRepresentationProperties;
}
@Override
@ -83,103 +65,12 @@ public class DefaultClientType implements ClientType {
}
@Override
public void onCreate(ClientRepresentation createdClient) throws ClientTypeException {
// Create empty client augmented with the applicable default client type values.
ClientRepresentation defaultClientRep = augmentClient(new ClientRepresentation());
validateClientRequest(createdClient, defaultClientRep);
augmentClient(createdClient);
public Map<String, ClientTypeRepresentation.PropertyConfig> getConfiguration() {
return clientType.getConfig();
}
@Override
public void onUpdate(ClientModel currentClient, ClientRepresentation newClient) throws ClientTypeException {
ClientRepresentation currentRep = ModelToRepresentation.toRepresentation(currentClient, session);
validateClientRequest(newClient, currentRep);
}
protected void validateClientRequest(ClientRepresentation newClient, ClientRepresentation currentClient) throws ClientTypeException {
List<String> validationErrors = clientType.getConfig().entrySet().stream()
.filter(property -> clientPropertyHasInvalidChangeRequested(currentClient, newClient, property.getKey(), property.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
if (validationErrors.size() > 0) {
throw new ClientTypeException(
"Cannot change property of client as it is not allowed by the specified client type.",
validationErrors.toArray());
}
}
protected ClientRepresentation augmentClient(ClientRepresentation client) {
clientType.getConfig().entrySet()
.forEach(property -> setClientProperty(client, property.getKey(), property.getValue()));
return client;
}
private boolean clientPropertyHasInvalidChangeRequested(
ClientRepresentation oldClient,
ClientRepresentation newClient,
String propertyName,
ClientTypeRepresentation.PropertyConfig propertyConfig) {
Object newClientProperty = getClientProperty(newClient, propertyName);
Object oldClientProperty = getClientProperty(oldClient, propertyName);
return (
// Validate that non-applicable client properties were not changed.
!propertyConfig.getApplicable() &&
!Objects.isNull(newClientProperty) &&
!Objects.equals(oldClientProperty, newClientProperty)
) || (
// Validate that applicable read-only client properties were not changed.
propertyConfig.getApplicable() &&
propertyConfig.getReadOnly() &&
!Objects.isNull(newClientProperty) &&
!Objects.equals(oldClientProperty, newClientProperty)
);
}
private void setClientProperty(ClientRepresentation client,
String propertyName,
ClientTypeRepresentation.PropertyConfig propertyConfig) {
if (!propertyConfig.getApplicable() || propertyConfig.getDefaultValue() == null) {
return;
}
if (clientRepresentationProperties.containsKey(propertyName)) {
// Java property on client representation
Method setter = clientRepresentationProperties.get(propertyName).getWriteMethod();
try {
setter.invoke(client, propertyConfig.getDefaultValue());
} catch (Exception e) {
logger.warnf("Cannot set property '%s' on client with value '%s'. Check configuration of the client type '%s'", propertyName, propertyConfig.getDefaultValue(), clientType.getName());
throw new ClientTypeException("Cannot set property on client", e);
}
} else {
// Client attribute
if (client.getAttributes() == null) {
client.setAttributes(new HashMap<>());
}
client.getAttributes().put(propertyName, propertyConfig.getDefaultValue().toString());
}
}
private Object getClientProperty(ClientRepresentation client, String propertyName) {
PropertyDescriptor propertyDescriptor = clientRepresentationProperties.get(propertyName);
if (propertyDescriptor != null) {
// Java property
Method getter = propertyDescriptor.getReadMethod();
try {
return getter.invoke(client);
} catch (Exception e) {
logger.warnf("Cannot read property '%s' on client '%s'. Client type is '%s'", propertyName, client.getClientId(), clientType.getName());
throw new ClientTypeException("Cannot read property of client", e);
}
} else {
// Attribute
return client.getAttributes() == null ? null : client.getAttributes().get(propertyName);
}
public ClientModel augment(ClientModel client) {
return new TypeAwareClientModelDelegate(this, () -> client);
}
}

View file

@ -18,11 +18,9 @@
package org.keycloak.services.clienttype.impl;
import java.beans.PropertyDescriptor;
import java.util.Map;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.representations.idm.ClientTypeRepresentation;
import org.keycloak.client.clienttype.ClientType;
import org.keycloak.client.clienttype.ClientTypeException;
@ -35,17 +33,9 @@ public class DefaultClientTypeProvider implements ClientTypeProvider {
private static final Logger logger = Logger.getLogger(DefaultClientTypeProvider.class);
private final KeycloakSession session;
private final Map<String, PropertyDescriptor> clientRepresentationProperties;
public DefaultClientTypeProvider(KeycloakSession session, Map<String, PropertyDescriptor> clientRepresentationProperties) {
this.session = session;
this.clientRepresentationProperties = clientRepresentationProperties;
}
@Override
public ClientType getClientType(ClientTypeRepresentation clientTypeRep) {
return new DefaultClientType(session, clientTypeRep, clientRepresentationProperties);
return new DefaultClientType(clientTypeRep);
}
@Override

View file

@ -18,23 +18,11 @@
package org.keycloak.services.clienttype.impl;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.client.clienttype.ClientTypeProvider;
import org.keycloak.client.clienttype.ClientTypeProviderFactory;
@ -45,41 +33,13 @@ public class DefaultClientTypeProviderFactory implements ClientTypeProviderFacto
public static final String PROVIDER_ID = "default";
private Map<String, PropertyDescriptor> clientRepresentationProperties;
@Override
public ClientTypeProvider create(KeycloakSession session) {
return new DefaultClientTypeProvider(session, clientRepresentationProperties);
return new DefaultClientTypeProvider();
}
@Override
public void init(Config.Scope config) {
Set<String> filtered = Arrays.stream(new String[] {"attributes", "type"}).collect(Collectors.toSet());
try {
BeanInfo bi = Introspector.getBeanInfo(ClientRepresentation.class);
PropertyDescriptor[] pd = bi.getPropertyDescriptors();
clientRepresentationProperties = Arrays.stream(pd)
.filter(desc -> !filtered.contains(desc.getName()))
.filter(desc -> desc.getWriteMethod() != null)
.map(desc -> {
// Take "is" methods into consideration
if (desc.getReadMethod() == null && Boolean.class.equals(desc.getPropertyType())) {
String methodName = "is" + desc.getName().substring(0, 1).toUpperCase() + desc.getName().substring(1);
try {
Method getter = ClientRepresentation.class.getDeclaredMethod(methodName);
desc.setReadMethod(getter);
} catch (Exception e) {
throw new IllegalStateException("Getter method for property " + desc.getName() + " cannot be found");
}
}
return desc;
})
.collect(Collectors.toMap(PropertyDescriptor::getName, Function.identity()));
} catch (IntrospectionException ie) {
throw new IllegalStateException("Introspection of Client representation failed", ie);
}
}
public void init(Config.Scope config) {}
@Override
public void postInit(KeycloakSessionFactory factory) {

View file

@ -42,8 +42,6 @@ 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;
@ -82,11 +80,6 @@ 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);

View file

@ -152,17 +152,6 @@ 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 -> {

View file

@ -6,8 +6,8 @@
"config": {
"standardFlowEnabled": {
"applicable": true,
"read-only": true,
"default-value": true
"default-value": true,
"read-only": true
}
}
},
@ -18,57 +18,67 @@
"alwaysDisplayInConsole": {
"applicable": false
},
"authorizationServicesEnabled": {
"applicable": false
},
"bearerOnly": {
"applicable": false
},
"consentRequired": {
"applicable": true,
"read-only": true,
"default-value": false
"default-value": false,
"read-only": true
},
"directAccessGrantsEnabled": {
"applicable": false
},
"frontchannelLogout": {
"applicable": false
},
"implicitFlowEnabled": {
"applicable": false
},
"login_theme": {
"applicable": false
},
"protocol": {
"applicable": true,
"read-only": true,
"default-value": "openid-connect"
},
"publicClient": {
"applicable": true,
"read-only": true,
"default-value": false
},
"bearerOnly": {
"applicable": true,
"read-only": true,
"default-value": false
},
"standardFlowEnabled": {
"applicable": true,
"read-only": true,
"default-value": false
},
"implicitFlowEnabled": {
"applicable": true,
"read-only": true,
"default-value": false
},
"directAccessGrantsEnabled": {
"applicable": true,
"read-only": true,
"default-value": false
},
"serviceAccountsEnabled": {
"applicable": true,
"read-only": true,
"default-value": true
},
"logoUri": {
"applicable": false
},
"oauth2.device.authorization.grant.enabled": {
"applicable": false
},
"oidc.ciba.grant.enabled": {
"applicable": false
},
"policyUri": {
"applicable": false
},
"protocol": {
"applicable": true,
"default-value": "openid-connect",
"read-only": true
},
"publicClient": {
"applicable": true,
"default-value": false,
"read-only": true
},
"redirectUris": {
"applicable": false
},
"serviceAccountsEnabled": {
"applicable": true,
"default-value": true,
"read-only": true
},
"standardFlowEnabled": {
"applicable": false
},
"tosUri": {
"applicable": false
},
"webOrigins": {
"applicable": false
}
}
}

View file

@ -126,14 +126,10 @@ public class ClientTypesTest extends AbstractTestRealmKeycloakTest {
clientRep.setServiceAccountsEnabled(true);
// Adding non-applicable attribute should not fail
// Adding non-applicable attribute should not fail but not update client attribute
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) {
assertErrorResponseContainsParams(bre.getResponse(), "logoUri");
}
assertEquals(testRealm().clients().get(clientRep.getId()).toRepresentation().getAttributes().get(ClientModel.LOGO_URI), null);
// Update of supported attribute should be successful
clientRep.getAttributes().remove(ClientModel.LOGO_URI);
@ -180,7 +176,7 @@ public class ClientTypesTest extends AbstractTestRealmKeycloakTest {
assertEquals("default", serviceAccountType.getProvider());
ClientTypeRepresentation.PropertyConfig cfg = serviceAccountType.getConfig().get("standardFlowEnabled");
assertPropertyConfig("standardFlowEnabled", cfg, true, true, false);
assertPropertyConfig("standardFlowEnabled", cfg, false, null, null);
cfg = serviceAccountType.getConfig().get("serviceAccountsEnabled");
assertPropertyConfig("serviceAccountsEnabled", cfg, true, true, true);